Compare commits

..

3 Commits

Author SHA1 Message Date
alex 98dd364ab0 lint 2023-03-05 10:19:46 +00:00
alex ebd1c2f564 support ^ inside strings again (for chords) 2023-03-05 10:16:27 +00:00
alex e15eef9a16 memories 2023-03-05 10:02:29 +00:00
191 changed files with 10102 additions and 21754 deletions
-62
View File
@@ -1,62 +0,0 @@
name: Tauri Builder
on:
push:
tags:
- "v*"
workflow_dispatch:
jobs:
release:
strategy:
fail-fast: false
matrix:
platform: [macos-latest, ubuntu-latest, windows-latest]
include:
- os: ubuntu-latest
rust_target: x86_64-unknown-linux-gnu
- os: macos-latest
rust_target: x86_64-apple-darwin
- os: macos-latest
rust_target: aarch64-apple-darwin
- os: windows-latest
rust_target: x86_64-pc-windows-msvc
runs-on: ${{ matrix.platform }}
steps:
- name: Checkout repository
uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
version: 8.6.2
- name: Node.js setup
uses: actions/setup-node@v3
with:
node-version: latest
# node-version-file: '.nvmrc'
- name: Install Rust (Stable)
run:
curl https://sh.rustup.rs -sSf | sh -s -- -y
- name: Install dependencies (ubuntu only)
if: matrix.platform == 'ubuntu-latest'
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf
- name: Install app dependencies from lockfile and build web
run: pnpm install
- name: Build the app
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
# tauri-action replaces \_\_VERSION\_\_ with the app version
tagName: ${{ github.ref_name }}
releaseName: "Strudel v__VERSION__"
releaseBody: |
See the assets to download this version and install.
releaseDraft: true
prerelease: false
-4
View File
@@ -40,7 +40,3 @@ repl/stats.html
coverage coverage
public/icons/apple-splash-* public/icons/apple-splash-*
dev-dist dev-dist
Dirt-Samples
tidal-drum-machines
webaudiofontdata
src-tauri/target
+2 -9
View File
@@ -31,7 +31,7 @@ Use one of the Communication Channels listed above.
## Improve the Docs ## Improve the Docs
If you find some weak spots in the [docs](https://strudel.tidalcycles.org/workshop/getting-started/), If you find some weak spots in the [docs](https://strudel.tidalcycles.org/learn/getting-started),
you can edit each file directly on github via the "Edit this page" link located in the right sidebar. you can edit each file directly on github via the "Edit this page" link located in the right sidebar.
## Propose a Feature ## Propose a Feature
@@ -123,14 +123,7 @@ To publish all packages that have been changed since the last release, run:
```sh ```sh
npm login npm login
npx lerna publish
# this will increment all the versions in package.json files of non private packages to selected versions
npx lerna version --no-private
# publish all packages inside /packages using pnpm! don't use lerna to publish!!
pnpm --filter "./packages/**" publish --dry-run
# the last command was only a dry-run, make sure everything looks ok, if yes, run the same command without flag
``` ```
To manually publish a single package, increase the version in the `package.json`, then run `pnpm publish`. To manually publish a single package, increase the version in the `package.json`, then run `pnpm publish`.
+2 -2
View File
@@ -14,8 +14,8 @@ An experiment in making a [Tidal](https://github.com/tidalcycles/tidal/) using w
After cloning the project, you can run the REPL locally: After cloning the project, you can run the REPL locally:
```bash ```bash
pnpm run setup npm run setup
pnpm run repl npm run repl
``` ```
## Using Strudel In Your Project ## Using Strudel In Your Project
+13 -11
View File
@@ -18,6 +18,7 @@
"build": "npm run prebuild && cd website && npm run build", "build": "npm run prebuild && cd website && npm run build",
"preview": "cd website && npm run preview", "preview": "cd website && npm run preview",
"osc": "cd packages/osc && npm run server", "osc": "cd packages/osc && npm run server",
"deploy": "NODE_DEBUG=gh-pages gh-pages -d out",
"jsdoc": "jsdoc packages/ -c jsdoc.config.json", "jsdoc": "jsdoc packages/ -c jsdoc.config.json",
"jsdoc-json": "jsdoc packages/ --template ./node_modules/jsdoc-json --destination doc.json -c jsdoc.config.json", "jsdoc-json": "jsdoc packages/ --template ./node_modules/jsdoc-json --destination doc.json -c jsdoc.config.json",
"lint": "eslint . --ext mjs,js --quiet", "lint": "eslint . --ext mjs,js --quiet",
@@ -52,21 +53,22 @@
"@strudel.cycles/webaudio": "workspace:*", "@strudel.cycles/webaudio": "workspace:*",
"@strudel.cycles/xen": "workspace:*", "@strudel.cycles/xen": "workspace:*",
"acorn": "^8.8.1", "acorn": "^8.8.1",
"dependency-tree": "^9.0.0" "dependency-tree": "^9.0.0",
"vitest": "^0.25.7"
}, },
"devDependencies": { "devDependencies": {
"@tauri-apps/cli": "^1.4.0", "@vitest/ui": "^0.25.7",
"@vitest/ui": "^0.28.0", "c8": "^7.12.0",
"canvas": "^2.11.2", "canvas": "^2.11.0",
"eslint": "^8.39.0", "eslint": "^8.28.0",
"eslint-plugin-import": "^2.27.5", "eslint-plugin-import": "^2.27.5",
"events": "^3.3.0", "events": "^3.3.0",
"jsdoc": "^4.0.2", "gh-pages": "^4.0.0",
"jsdoc": "^3.6.10",
"jsdoc-json": "^2.0.2", "jsdoc-json": "^2.0.2",
"jsdoc-to-markdown": "^8.0.0", "jsdoc-to-markdown": "^7.1.1",
"lerna": "^6.6.1", "lerna": "^4.0.0",
"prettier": "^2.8.8", "prettier": "^2.8.1",
"rollup-plugin-visualizer": "^5.8.1", "rollup-plugin-visualizer": "^5.8.1"
"vitest": "^0.28.0"
} }
} }
-3
View File
@@ -1,3 +0,0 @@
# @strudel/codemirror
This package contains helpers and extensions to use codemirror6. See [vite-vanilla-repl-cm6](../core/examples/vite-vanilla-repl-cm6/main.js) as an example of using it.
-199
View File
@@ -1,199 +0,0 @@
import { EditorState } from '@codemirror/state';
import { EditorView, keymap, Decoration, lineNumbers, highlightActiveLineGutter } from '@codemirror/view';
import { defaultKeymap } from '@codemirror/commands';
import { syntaxHighlighting, defaultHighlightStyle } from '@codemirror/language';
import { javascript } from '@codemirror/lang-javascript';
import { StateField, StateEffect } from '@codemirror/state';
import { oneDark } from './themes/one-dark';
import { repl, Drawer } from '@strudel.cycles/core';
// https://codemirror.net/docs/guide/
export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, theme = oneDark, root }) {
let state = EditorState.create({
doc: initialCode,
extensions: [
theme,
javascript(),
lineNumbers(),
highlightField,
highlightActiveLineGutter(),
syntaxHighlighting(defaultHighlightStyle),
keymap.of(defaultKeymap),
flashField,
EditorView.updateListener.of((v) => onChange(v)),
keymap.of([
{
key: 'Ctrl-Enter',
run: () => onEvaluate(),
},
{
key: 'Ctrl-.',
run: () => onStop(),
},
]),
],
});
return new EditorView({
state,
parent: root,
});
}
//
// highlighting
//
export const setHighlights = StateEffect.define();
export const highlightField = StateField.define({
create() {
return Decoration.none;
},
update(highlights, tr) {
try {
for (let e of tr.effects) {
if (e.is(setHighlights)) {
const { haps } = e.value;
const marks =
haps
.map((hap) =>
(hap.context.locations || []).map(({ start, end }) => {
// const color = hap.context.color || e.value.color || '#FFCA28';
let from = tr.newDoc.line(start.line).from + start.column;
let to = tr.newDoc.line(end.line).from + end.column;
const l = tr.newDoc.length;
if (from > l || to > l) {
return; // dont mark outside of range, as it will throw an error
}
const mark = Decoration.mark({
attributes: { style: `outline: 2px solid #FFCA28;` },
});
return mark.range(from, to);
}),
)
.flat()
.filter(Boolean) || [];
highlights = Decoration.set(marks, true);
}
}
return highlights;
} catch (err) {
// console.warn('highlighting error', err);
return Decoration.set([]);
}
},
provide: (f) => EditorView.decorations.from(f),
});
// helper to simply trigger highlighting for given haps
export function highlightHaps(view, haps) {
view.dispatch({ effects: setHighlights.of({ haps }) });
}
//
// flash
//
export const setFlash = StateEffect.define();
const flashField = StateField.define({
create() {
return Decoration.none;
},
update(flash, tr) {
try {
for (let e of tr.effects) {
if (e.is(setFlash)) {
if (e.value) {
const mark = Decoration.mark({ attributes: { style: `background-color: #FFCA2880` } });
flash = Decoration.set([mark.range(0, tr.newDoc.length)]);
} else {
flash = Decoration.set([]);
}
}
}
return flash;
} catch (err) {
console.warn('flash error', err);
return flash;
}
},
provide: (f) => EditorView.decorations.from(f),
});
export const flash = (view, ms = 200) => {
view.dispatch({ effects: setFlash.of(true) });
setTimeout(() => {
view.dispatch({ effects: setFlash.of(false) });
}, ms);
};
export class StrudelMirror {
constructor(options) {
const { root, initialCode = '', onDraw, drawTime = [-2, 2], prebake, ...replOptions } = options;
this.code = initialCode;
this.drawer = new Drawer((haps, time) => {
const currentFrame = haps.filter((hap) => time >= hap.whole.begin && time <= hap.endClipped);
this.highlight(currentFrame);
onDraw?.(haps, time, currentFrame);
}, drawTime);
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: async (started) => {
replOptions?.onToggle?.(started);
const { scheduler } = await this.repl;
if (started) {
this.drawer.start(scheduler);
} else {
this.drawer.stop();
}
},
beforeEval: async () => {
await prebaked;
},
afterEval: (options) => {
replOptions?.afterEval?.(options);
this.drawer.invalidate();
},
});
this.editor = initEditor({
root,
initialCode,
onChange: (v) => {
this.code = v.state.doc.toString();
},
onEvaluate: () => this.evaluate(),
onStop: () => this.stop(),
});
}
async evaluate() {
const { evaluate } = await this.repl;
this.flash();
await evaluate(this.code);
}
async stop() {
const { scheduler } = await this.repl;
scheduler.stop();
}
flash(ms) {
flash(this.editor, ms);
}
highlight(haps) {
highlightHaps(this.editor, haps);
}
}
-47
View File
@@ -1,47 +0,0 @@
{
"name": "@strudel/codemirror",
"version": "0.8.4",
"description": "Codemirror Extensions for Strudel",
"main": "codemirror.mjs",
"publishConfig": {
"main": "dist/index.js",
"module": "dist/index.mjs"
},
"scripts": {
"build": "vite build",
"prepublishOnly": "npm run build"
},
"type": "module",
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
},
"keywords": [
"tidalcycles",
"strudel",
"pattern",
"livecoding",
"algorave"
],
"author": "Felix Roos <flix91@gmail.com>",
"contributors": [
"Alex McLean <alex@slab.org>"
],
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@codemirror/commands": "^6.2.4",
"@codemirror/lang-javascript": "^6.1.7",
"@codemirror/language": "^6.6.0",
"@codemirror/state": "^6.2.0",
"@codemirror/view": "^6.10.0",
"@lezer/highlight": "^1.1.4",
"@strudel.cycles/core": "workspace:*"
},
"devDependencies": {
"vite": "^4.3.3"
}
}
-139
View File
@@ -1,139 +0,0 @@
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)];
-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, 'codemirror.mjs'),
formats: ['es', 'cjs'],
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
},
rollupOptions: {
external: [...Object.keys(dependencies)],
},
target: 'esnext',
},
});
+1 -2
View File
@@ -1,5 +1,4 @@
import { Pattern, getDrawContext, silence, register, pure } from './index.mjs'; import { controls, Pattern, getDrawContext, silence, register, pure } from './index.mjs';
import controls from './controls.mjs'; // do not import from index.mjs as it breaks for some reason..
const { createParams } = controls; const { createParams } = controls;
let clearColor = '#22222210'; let clearColor = '#22222210';
+5 -27
View File
@@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Pattern, register, sequence } from './pattern.mjs'; import { Pattern, sequence } from './pattern.mjs';
import { zipWith } from './util.mjs'; import { zipWith } from './util.mjs';
const controls = {}; const controls = {};
@@ -23,15 +23,6 @@ const generic_params = [
* *
*/ */
[['s', 'n', 'gain'], 'sound'], [['s', 'n', 'gain'], 'sound'],
/**
* Define a custom webaudio node to use as a sound source.
*
* @name source
* @param {function} getSource
* @synonyms src
*
*/
['source', 'src'],
/** /**
* Selects the given index from the sample map. * Selects the given index from the sample map.
* Numbers too high will wrap around. * Numbers too high will wrap around.
@@ -222,7 +213,6 @@ const generic_params = [
* *
* @name legato * @name legato
* @param {number | Pattern} duration between 0 and 1, where 1 is the length of the whole hap time * @param {number | Pattern} duration between 0 and 1, where 1 is the length of the whole hap time
* @noAutocomplete
* @example * @example
* "c4 eb4 g4 bb4".legato("<0.125 .25 .5 .75 1 2 4>") * "c4 eb4 g4 bb4".legato("<0.125 .25 .5 .75 1 2 4>")
* *
@@ -750,14 +740,13 @@ const generic_params = [
['val'], ['val'],
['cps'], ['cps'],
/** /**
* Multiplies the duration with the given number. Also cuts samples off at the end if they exceed the duration. * If set to 1, samples will be cut to the duration of their event.
* In tidal, this would be done with legato, [which has a complicated history in strudel](https://github.com/tidalcycles/strudel/issues/111). * In tidal, this would be done with legato, which [is about to land in strudel too](https://github.com/tidalcycles/strudel/issues/111)
* For now, if you're coming from tidal, just think clip = legato.
* *
* @name clip * @name clip
* @param {number | Pattern} factor >= 0 * @param {number | Pattern} active 1 or 0
* @example * @example
* note("c a f e").s("piano").clip("<.5 1 2>") * note("c a f e ~").s("piano").clip(1)
* *
*/ */
['clip'], ['clip'],
@@ -812,15 +801,4 @@ generic_params.forEach(([names, ...aliases]) => {
controls.createParams = (...names) => controls.createParams = (...names) =>
names.reduce((acc, name) => Object.assign(acc, { [name]: controls.createParam(name) }), {}); names.reduce((acc, name) => Object.assign(acc, { [name]: controls.createParam(name) }), {});
controls.adsr = register('adsr', (adsr, pat) => {
adsr = !Array.isArray(adsr) ? [adsr] : adsr;
const [attack, decay, sustain, release] = adsr;
return pat.set({ attack, decay, sustain, release });
});
controls.ds = register('ds', (ds, pat) => {
ds = !Array.isArray(ds) ? [ds] : ds;
const [decay, sustain] = ds;
return pat.set({ decay, sustain });
});
export default controls; export default controls;
-1
View File
@@ -74,7 +74,6 @@ export class Cyclist {
stop() { stop() {
logger('[cyclist] stop'); logger('[cyclist] stop');
this.clock.stop(); this.clock.stop();
this.lastEnd = 0;
this.setStarted(false); this.setStarted(false);
} }
setPattern(pat, autostart = false) { setPattern(pat, autostart = false) {
+2 -96
View File
@@ -1,6 +1,6 @@
/* /*
draw.mjs - <short description TODO> draw.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/draw.mjs> Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/tone/draw.mjs>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
@@ -13,7 +13,7 @@ export const getDrawContext = (id = 'test-canvas') => {
canvas.id = id; canvas.id = id;
canvas.width = window.innerWidth; canvas.width = window.innerWidth;
canvas.height = window.innerHeight; canvas.height = window.innerHeight;
canvas.style = 'pointer-events:none;width:100%;height:100%;position:fixed;top:0;left:0'; canvas.style = 'pointer-events:none;width:100%;height:100%;position:fixed;top:0;left:0;z-index:5';
document.body.prepend(canvas); document.body.prepend(canvas);
} }
return canvas.getContext('2d'); return canvas.getContext('2d');
@@ -65,97 +65,3 @@ Pattern.prototype.onPaint = function (onPaint) {
this.context = { onPaint }; this.context = { onPaint };
return this; return this;
}; };
// const round = (x) => Math.round(x * 1000) / 1000;
// encapsulates starting and stopping animation frames
export class Framer {
constructor(onFrame, onError) {
this.onFrame = onFrame;
this.onError = onError;
}
start() {
const self = this;
let frame = requestAnimationFrame(function updateHighlights(time) {
try {
self.onFrame(time);
} catch (err) {
self.onError(err);
}
frame = requestAnimationFrame(updateHighlights);
});
self.cancel = () => {
cancelAnimationFrame(frame);
};
}
stop() {
if (this.cancel) {
this.cancel();
}
}
}
// syncs animation frames to a cyclist scheduler
// see vite-vanilla-repl-cm6 for an example
export class Drawer {
constructor(onDraw, drawTime) {
let [lookbehind, lookahead] = drawTime; // e.g. [-2, 2]
lookbehind = Math.abs(lookbehind);
this.visibleHaps = [];
this.lastFrame = null;
this.drawTime = drawTime;
this.framer = new Framer(
() => {
if (!this.scheduler) {
console.warn('Drawer: no scheduler');
return;
}
// calculate current frame time (think right side of screen for pianoroll)
const phase = this.scheduler.now() + lookahead;
// first frame just captures the phase
if (this.lastFrame === null) {
this.lastFrame = phase;
return;
}
// query haps from last frame till now. take last 100ms max
const haps = this.scheduler.pattern.queryArc(Math.max(this.lastFrame, phase - 1 / 10), phase);
this.lastFrame = phase;
this.visibleHaps = (this.visibleHaps || [])
// filter out haps that are too far in the past (think left edge of screen for pianoroll)
.filter((h) => h.whole.end >= phase - lookbehind - lookahead)
// add new haps with onset (think right edge bars scrolling in)
.concat(haps.filter((h) => h.hasOnset()));
const time = phase - lookahead;
onDraw(this.visibleHaps, time, this);
},
(err) => {
console.warn('draw error', err);
},
);
}
invalidate(scheduler = this.scheduler) {
if (!scheduler) {
return;
}
this.scheduler = scheduler;
const t = scheduler.now();
let [_, lookahead] = this.drawTime;
const [begin, end] = [Math.max(t, 0), t + lookahead + 0.1];
// remove all future haps
this.visibleHaps = this.visibleHaps.filter((h) => h.whole.begin < t);
// query future haps
const futureHaps = scheduler.pattern.queryArc(begin, end); // +0.1 = workaround for weird holes in query..
// append future haps
this.visibleHaps = this.visibleHaps.concat(futureHaps);
}
start(scheduler) {
this.scheduler = scheduler;
this.invalidate();
this.framer.start();
}
stop() {
if (this.framer) {
this.framer.stop();
}
}
}
@@ -1,24 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
@@ -1,8 +0,0 @@
# vite-vanilla-repl-cm6
This folder demonstrates how to set up a strudel repl using vite and vanilla JS + codemirror. Run it using:
```sh
npm i
npm run dev
```
@@ -1,22 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite Vanilla Strudel REPL</title>
</head>
<body>
<main>
<nav>
<button id="play">eval</button>
<button id="stop">stop</button>
</nav>
<div class="container">
<div id="editor"></div>
<div id="output"></div>
</div>
<canvas id="roll"></canvas>
</main>
<script type="module" src="./main.js"></script>
</body>
</html>
@@ -1,39 +0,0 @@
import { StrudelMirror } from '@strudel/codemirror';
import { funk42 } from './tunes';
import { drawPianoroll, evalScope, controls } from '@strudel.cycles/core';
import './style.css';
import { initAudioOnFirstClick } from '@strudel.cycles/webaudio';
import { transpiler } from '@strudel.cycles/transpiler';
import { getAudioContext, webaudioOutput, registerSynthSounds } from '@strudel.cycles/webaudio';
import { registerSoundfonts } from '@strudel.cycles/soundfonts';
// init canvas
const canvas = document.getElementById('roll');
canvas.width = canvas.width * 2;
canvas.height = canvas.height * 2;
const drawContext = canvas.getContext('2d');
const drawTime = [-2, 2]; // time window of drawn haps
const editor = new StrudelMirror({
defaultOutput: webaudioOutput,
getTime: () => getAudioContext().currentTime,
transpiler,
root: document.getElementById('editor'),
initialCode: funk42,
drawTime,
onDraw: (haps, time) => drawPianoroll({ haps, time, ctx: drawContext, drawTime, fold: 0 }),
prebake: async () => {
initAudioOnFirstClick(); // needed to make the browser happy (don't await this here..)
const loadModules = evalScope(
controls,
import('@strudel.cycles/core'),
import('@strudel.cycles/mini'),
import('@strudel.cycles/tonal'),
import('@strudel.cycles/webaudio'),
);
await Promise.all([loadModules, registerSynthSounds(), registerSoundfonts()]);
},
});
document.getElementById('play').addEventListener('click', () => editor.evaluate());
document.getElementById('stop').addEventListener('click', () => editor.stop());
@@ -1,23 +0,0 @@
{
"name": "vite-vanilla-repl-cm6",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"vite": "^4.3.2"
},
"dependencies": {
"@strudel/codemirror": "workspace:*",
"@strudel.cycles/core": "workspace:*",
"@strudel.cycles/mini": "workspace:*",
"@strudel.cycles/soundfonts": "workspace:*",
"@strudel.cycles/tonal": "workspace:*",
"@strudel.cycles/transpiler": "workspace:*",
"@strudel.cycles/webaudio": "workspace:*"
}
}
@@ -1,31 +0,0 @@
body,
html {
margin: 0;
height: 100%;
background: #282c34;
}
main {
height: 100%;
display: flex;
flex-direction: column;
}
.container {
flex-grow: 1;
max-height: 100%;
position: relative;
overflow: auto;
}
#editor {
overflow: auto;
}
.cm-editor {
height: 100%;
}
#roll {
height: 300px;
}
@@ -1,112 +0,0 @@
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.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
.fmap(v => Math.pow(2,v/22))
// mess with the base frequency
.mul("<300 [300@3 200]>/8").freq()
.layer(
// chords
x=>x.div(freq(2)).s("flute").euclidLegato("<3 2>",8)
.shape(.4).lpf(sine.range(800,4000).slow(8)),
// adlibs
x=>x.arp("{0 3 2 [1 3]}%1.5")
.s('xylo').mul(freq(2))
.delay(.5).delayfeedback(.4).juxBy(.5, rev)
.hpf(sine.range(200,3000).slow(8)),
// bass
x=>x.arp("[0 [2 1?]](5,8)").s('sawtooth').div(freq(4))
.lpf(sine.range(400,2000).slow(8)).lpq(8).shape(.4)
.off(1/8, x=>x.mul(freq(2)).degradeBy(.5)).gain(.3)
).clip(1).release(.2)
.stack(
// drums
s("bd sd:<2 1>, [~ hh]*2, [~ rim]").bank('RolandTR909')
.off(1/8, x=>x.speed(2).gain(.4)).sometimes(ply(2)).gain(.8)
.mask("<0@4 1@12>/4")
.reset("<x@15 [x(3,8) x*[4 8]]>")
// wait for it...
).fast(2/3)
//.crush(6) // remove "//" if you dare`;
export const trafficFlam = `// froos - "traffic flam", licensed with CC BY-NC-SA 4.0
await samples('github:felixroos/samples/main')
await samples('https://strudel.tidalcycles.org/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
addVoicings('hip', {
m11: ['2M 3m 4P 7m'],
'^7#11': ['3M 4A 5P 7M'],
}, ['C4', 'C6'])
stack(
stack(
"<Bbm11 A^7#11>/2".voicings('hip').note()
.s("gm_epiano1:2")
.arp("[<[0 1 2 3] [3 2 1 0]> ~@5]/2")
.release(2).late(.25).lpf(2000),
"<Bb1 A1>/2".note().s('gm_acoustic_bass'),
n("<0 2 3>(3,8)".off(1/8, add(4)))
.scale("<Bb4:minor A4:lydian>/2")
.s('gm_electric_guitar_jazz')
.decay(sine.range(.05, .2).slow(32)).sustain(0)
.delay(.5).lpf(sine.range(100,5000).slow(64))
.gain(.7).room(.5).pan(sine.range(0,1).slow(11))
).add(perlin.range(0,.25).note()),
stack(
s("bd:1(3,8) rim").bank('RolandTR707').slow(2).room("<0 <.1 .6>>")
.when("<0@7 1>",x=>x.echoWith(3, .0625, (x,i) => x.speed(1+i*.24))),
s("rim*4").end(.05).bank('RolandTR808').speed(.8).room(.2)
)
)
.late("[0 .05]*2").late(12)
`;
export const funk42 = `// froos - how to funk in 42 lines of code
// adapted from "how to funk in two minutes" by marc rebillet https://www.youtube.com/watch?v=3vBwRfQbXkg
// thanks to peach for the transcription: https://www.youtube.com/watch?v=8eiPXvIgda4
await samples('github:felixroos/samples/main')
await samples('https://strudel.tidalcycles.org/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
setcps(.5)
let drums = stack(
s("bd*2, ~ sd").bank('RolandTR707').room("0 .1"),
s("hh*4").begin(.2).release(.02).end(.25).release(.02)
.gain(.3).bank('RolandTR707').late(.02).room(.5),
//s("shaker_small").struct("[x x*2]*2").speed(".8,.9").release(.02)
).fast(2)
let wurli = note(\`<
[[a2,g3,[b3 c4],e4] ~ [g3,c4,e4](3,8)@4 ~@2]!3
[[e2,e3,a3,b3,e4]@3 [e2,e3,ab3,b3,e4]@5]>\`)
.s("gm_epiano1:5").decay(.2).sustain("<[1 0@7]!3 1>")
.gain("<[.8@2 .4@14]!3 .7>").room(.3)
let organ = note("<[~@3 [a3,d4,f#4]@2 [[a3,c4,e4]@2 ~] ~@2]!3 ~>".add(12))
.s("gm_percussive_organ:2").gain(.6).lpf(1800).pan(.2).room(.3);
let clav = note(\`<
[~@3 a2 [g3,[b3 c4],e4]@2 ~ a2 [g3,b3,e4] ~@2 [g3,c4,e4] ~@4]!3
[~@3 e3 [[a3 b3],c3,e3]@2 ~ e2 [e3,a3]@3 [b3,e3] ~@2 [b3,e3]@2]>\`)
.s("gm_clavinet:1").decay("<.25!3 [.25 .4]>").sustain(0)
.gain(.7).pan(.8).room(.2);
let bass = note(\`<
[a1 [~ [g2 a2]] [g1 g#1] [a1 [g2 a2]]]
[a1 [~ [g2 a2]] [e3 d3] [c3 [g3 a3]]]
[a1 [~ [g2 a2]] [g1 g#1] [a1 [g2 a2]]]
[e2@6 e1@5 e1 [[d2 e3] g1]@4]
>\`).s("gm_electric_bass_pick:1").release(.1)
stack(
drums
,wurli
,organ
,clav
,bass
)`;
@@ -6,3 +6,5 @@ This folder demonstrates how to set up a strudel repl using vite and vanilla JS.
npm i npm i
npm run dev npm run dev
``` ```
or view it [live on githack](https://rawcdn.githack.com/tidalcycles/strudel/5fb36acb046ead7cd6ad3cd10f532e7f585f536a/packages/core/examples/vite-vanilla-repl/dist/index.html)
@@ -10,7 +10,7 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"devDependencies": { "devDependencies": {
"vite": "^4.3.3" "vite": "^3.2.0"
}, },
"dependencies": { "dependencies": {
"@strudel.cycles/core": "workspace:*", "@strudel.cycles/core": "workspace:*",
+1 -5
View File
@@ -32,11 +32,7 @@ export class Hap {
} }
get duration() { get duration() {
return this.whole.end.sub(this.whole.begin).mul(typeof this.value?.clip === 'number' ? this.value?.clip : 1); return this.whole.end.sub(this.whole.begin);
}
get endClipped() {
return this.whole.begin.add(this.duration);
} }
wholeOrPart() { wholeOrPart() {
-2
View File
@@ -18,13 +18,11 @@ export * from './util.mjs';
export * from './speak.mjs'; export * from './speak.mjs';
export * from './evaluate.mjs'; export * from './evaluate.mjs';
export * from './repl.mjs'; export * from './repl.mjs';
export * from './cyclist.mjs';
export * from './logger.mjs'; export * from './logger.mjs';
export * from './time.mjs'; export * from './time.mjs';
export * from './draw.mjs'; export * from './draw.mjs';
export * from './animate.mjs'; export * from './animate.mjs';
export * from './pianoroll.mjs'; export * from './pianoroll.mjs';
export * from './spiral.mjs';
export * from './ui.mjs'; export * from './ui.mjs';
export { default as drawLine } from './drawLine.mjs'; export { default as drawLine } from './drawLine.mjs';
export { default as gist } from './gist.js'; export { default as gist } from './gist.js';
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel.cycles/core", "name": "@strudel.cycles/core",
"version": "0.8.2", "version": "0.6.8",
"description": "Port of Tidal Cycles to JavaScript", "description": "Port of Tidal Cycles to JavaScript",
"main": "index.mjs", "main": "index.mjs",
"type": "module", "type": "module",
@@ -35,7 +35,7 @@
}, },
"gitHead": "0e26d4e741500f5bae35b023608f062a794905c2", "gitHead": "0e26d4e741500f5bae35b023608f062a794905c2",
"devDependencies": { "devDependencies": {
"vite": "^4.3.3", "vite": "^3.2.2",
"vitest": "^0.28.0" "vitest": "^0.25.7"
} }
} }
+10 -49
View File
@@ -1127,8 +1127,6 @@ function _composeOp(a, b, func) {
export const polyrhythm = stack; export const polyrhythm = stack;
export const pr = stack; export const pr = stack;
export const pm = polymeter;
// methods that create patterns, which are added to patternified Pattern methods // methods that create patterns, which are added to patternified Pattern methods
// TODO: remove? this is only used in old transpiler (shapeshifter) // TODO: remove? this is only used in old transpiler (shapeshifter)
Pattern.prototype.factories = { Pattern.prototype.factories = {
@@ -1286,20 +1284,6 @@ export function timeCat(...timepats) {
return stack(...pats); return stack(...pats);
} }
/**
* Allows to arrange multiple patterns together over multiple cycles.
* Takes a variable number of arrays with two elements specifying the number of cycles and the pattern to use.
*
* @return {Pattern}
* @example
* arrange([4, "<c a f e>(3,8)"],[2, "<g a>(5,8)"]).note()
*/
export function arrange(...sections) {
const total = sections.reduce((sum, [cycles]) => sum + cycles, 0);
sections = sections.map(([cycles, section]) => [cycles, section.fast(cycles)]);
return timeCat(...sections).slow(total);
}
export function fastcat(...pats) { export function fastcat(...pats) {
return slowcat(...pats)._fast(pats.length); return slowcat(...pats)._fast(pats.length);
} }
@@ -1376,6 +1360,11 @@ export function polymeter(...args) {
return polymeterSteps(0, ...args); return polymeterSteps(0, ...args);
} }
// alias
export function pm(...args) {
polymeter(...args);
}
export const mask = curry((a, b) => reify(b).mask(a)); export const mask = curry((a, b) => reify(b).mask(a));
export const struct = curry((a, b) => reify(b).struct(a)); export const struct = curry((a, b) => reify(b).struct(a));
export const superimpose = curry((a, b) => reify(b).superimpose(...a)); export const superimpose = curry((a, b) => reify(b).superimpose(...a));
@@ -1582,24 +1571,6 @@ export const range2 = register('range2', function (min, max, pat) {
return pat.fromBipolar()._range(min, max); return pat.fromBipolar()._range(min, max);
}); });
/**
* Allows dividing numbers via list notation using ":".
* Returns a new pattern with just numbers.
* @name ratio
* @memberof Pattern
* @returns Pattern
* @example
* ratio("1, 5:4, 3:2").mul(110).freq().s("piano").slow(2)
*/
export const ratio = register('ratio', (pat) =>
pat.fmap((v) => {
if (!Array.isArray(v)) {
return v;
}
return v.slice(1).reduce((acc, n) => acc / n, v[0]);
}),
);
////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////
// Structural and temporal transformations // Structural and temporal transformations
@@ -1695,9 +1666,6 @@ export const ply = register('ply', function (factor, pat) {
* s("<bd sd> hh").fast(2) // s("[<bd sd> hh]*2") * s("<bd sd> hh").fast(2) // s("[<bd sd> hh]*2")
*/ */
export const { fast, density } = register(['fast', 'density'], function (factor, pat) { export const { fast, density } = register(['fast', 'density'], function (factor, pat) {
if (factor === 0) {
return silence;
}
factor = Fraction(factor); factor = Fraction(factor);
const fastQuery = pat.withQueryTime((t) => t.mul(factor)); const fastQuery = pat.withQueryTime((t) => t.mul(factor));
return fastQuery.withHapTime((t) => t.div(factor)); return fastQuery.withHapTime((t) => t.div(factor));
@@ -1724,9 +1692,6 @@ export const hurry = register('hurry', function (r, pat) {
* s("<bd sd> hh").slow(2) // s("[<bd sd> hh]/2") * s("<bd sd> hh").slow(2) // s("[<bd sd> hh]/2")
*/ */
export const { slow, sparsity } = register(['slow', 'sparsity'], function (factor, pat) { export const { slow, sparsity } = register(['slow', 'sparsity'], function (factor, pat) {
if (factor === 0) {
return silence;
}
return pat._fast(Fraction(1).div(factor)); return pat._fast(Fraction(1).div(factor));
}); });
@@ -2053,7 +2018,7 @@ export const jux = register('jux', function (func, pat) {
* @example * @example
* "<0 [2 4]>" * "<0 [2 4]>"
* .echoWith(4, 1/8, (p,n) => p.add(n*2)) * .echoWith(4, 1/8, (p,n) => p.add(n*2))
* .scale('C minor').note().clip(.2) * .scale('C minor').note().legato(.2)
*/ */
export const { echoWith, echowith, stutWith, stutwith } = register( export const { echoWith, echowith, stutWith, stutwith } = register(
['echoWith', 'echowith', 'stutWith', 'stutwith'], ['echoWith', 'echowith', 'stutWith', 'stutwith'],
@@ -2203,7 +2168,6 @@ export const velocity = register('velocity', function (velocity, pat) {
/** /**
* *
* Multiplies the hap duration with the given factor. * Multiplies the hap duration with the given factor.
* With samples, `clip` might be a better function to use ([more info](https://github.com/tidalcycles/strudel/pull/598))
* @name legato * @name legato
* @memberof Pattern * @memberof Pattern
* @example * @example
@@ -2276,7 +2240,7 @@ const _loopAt = function (factor, pat, cps = 1) {
* s("breaks165").slice(8, "0 1 <2 2*2> 3 [4 0] 5 6 7".every(3, rev)).slow(1.5) * s("breaks165").slice(8, "0 1 <2 2*2> 3 [4 0] 5 6 7".every(3, rev)).slow(1.5)
*/ */
export const slice = register( const slice = register(
'slice', 'slice',
function (npat, ipat, opat) { function (npat, ipat, opat) {
return npat.innerBind((n) => return npat.innerBind((n) =>
@@ -2304,7 +2268,7 @@ export const slice = register(
* s("breaks165").splice(8, "0 1 [2 3 0]@2 3 0@2 7").hurry(0.65) * s("breaks165").splice(8, "0 1 [2 3 0]@2 3 0@2 7").hurry(0.65)
*/ */
export const splice = register( const splice = register(
'splice', 'splice',
function (npat, ipat, opat) { function (npat, ipat, opat) {
const sliced = slice(npat, ipat, opat); const sliced = slice(npat, ipat, opat);
@@ -2321,10 +2285,7 @@ export const splice = register(
false, // turns off auto-patternification false, // turns off auto-patternification
); );
// this function will be redefined in repl.mjs to use the correct cps value. const { loopAt, loopat } = register(['loopAt', 'loopat'], function (factor, pat) {
// It is still here to work in cases where repl.mjs is not used
export const { loopAt, loopat } = register(['loopAt', 'loopat'], function (factor, pat) {
return _loopAt(factor, pat, 1); return _loopAt(factor, pat, 1);
}); });
@@ -2341,6 +2302,6 @@ export const { loopAt, loopat } = register(['loopAt', 'loopat'], function (facto
* s("rhodes").loopAtCps(4,1.5).cps(1.5) * s("rhodes").loopAtCps(4,1.5).cps(1.5)
*/ */
// TODO - global cps clock // TODO - global cps clock
export const { loopAtCps, loopatcps } = register(['loopAtCps', 'loopatcps'], function (factor, cps, pat) { const { loopAtCps, loopatcps } = register(['loopAtCps', 'loopatcps'], function (factor, cps, pat) {
return _loopAt(factor, pat, cps); return _loopAt(factor, pat, cps);
}); });
+8 -39
View File
@@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Pattern, noteToMidi, getDrawContext, freqToMidi, isNote } from './index.mjs'; import { Pattern, toMidi, getDrawContext, freqToMidi, isNote } from './index.mjs';
const scale = (normalized, min, max) => normalized * (max - min) + min; const scale = (normalized, min, max) => normalized * (max - min) + min;
const getValue = (e) => { const getValue = (e) => {
@@ -18,7 +18,7 @@ const getValue = (e) => {
} }
note = note ?? n; note = note ?? n;
if (typeof note === 'string') { if (typeof note === 'string') {
return noteToMidi(note); return toMidi(note);
} }
if (typeof note === 'number') { if (typeof note === 'number') {
return note; return note;
@@ -50,7 +50,6 @@ Pattern.prototype.pianoroll = function ({
timeframe: timeframeProp, timeframe: timeframeProp,
fold = 0, fold = 0,
vertical = 0, vertical = 0,
labels = 0,
} = {}) { } = {}) {
const ctx = getDrawContext(); const ctx = getDrawContext();
const w = ctx.canvas.width; const w = ctx.canvas.width;
@@ -83,12 +82,12 @@ Pattern.prototype.pianoroll = function ({
ctx.fillRect(0, 0, w, h); ctx.fillRect(0, 0, w, h);
} }
const inFrame = (event) => const inFrame = (event) =>
(!hideNegative || event.whole.begin >= 0) && event.whole.begin <= t + to && event.endClipped >= t + from; (!hideNegative || event.whole.begin >= 0) && event.whole.begin <= t + to && event.whole.end >= t + from;
events.filter(inFrame).forEach((event) => { events.filter(inFrame).forEach((event) => {
const isActive = event.whole.begin <= t && event.endClipped > t; const isActive = event.whole.begin <= t && event.whole.end > t;
ctx.fillStyle = event.context?.color || inactive; ctx.fillStyle = event.context?.color || inactive;
ctx.strokeStyle = event.context?.color || active; ctx.strokeStyle = event.context?.color || active;
ctx.globalAlpha = event.context.velocity ?? event.value?.gain ?? 1; ctx.globalAlpha = event.context.velocity ?? 1;
const timePx = scale((event.whole.begin - (flipTime ? to : from)) / timeExtent, ...timeRange); const timePx = scale((event.whole.begin - (flipTime ? to : from)) / timeExtent, ...timeRange);
let durationPx = scale(event.duration / timeExtent, 0, timeAxis); let durationPx = scale(event.duration / timeExtent, 0, timeAxis);
const value = getValue(event); const value = getValue(event);
@@ -115,14 +114,6 @@ Pattern.prototype.pianoroll = function ({
]; ];
} }
isActive ? ctx.strokeRect(...coords) : ctx.fillRect(...coords); isActive ? ctx.strokeRect(...coords) : ctx.fillRect(...coords);
if (labels) {
const label = event.value.note ?? event.value.s + (event.value.n ? `:${event.value.n}` : '');
ctx.font = `${barThickness * 0.75}px monospace`;
ctx.strokeStyle = 'black';
ctx.fillStyle = isActive ? 'white' : 'black';
ctx.textBaseline = 'top';
ctx.fillText(label, ...coords);
}
}); });
ctx.globalAlpha = 1; // reset! ctx.globalAlpha = 1; // reset!
const playheadPosition = scale(-from / timeExtent, ...timeRange); const playheadPosition = scale(-from / timeExtent, ...timeRange);
@@ -190,7 +181,6 @@ export function pianoroll({
timeframe: timeframeProp, timeframe: timeframeProp,
fold = 0, fold = 0,
vertical = 0, vertical = 0,
labels = false,
ctx, ctx,
} = {}) { } = {}) {
const w = ctx.canvas.width; const w = ctx.canvas.width;
@@ -250,7 +240,7 @@ export function pianoroll({
const color = event.value?.color || event.context?.color; const color = event.value?.color || event.context?.color;
ctx.fillStyle = color || inactive; ctx.fillStyle = color || inactive;
ctx.strokeStyle = color || active; ctx.strokeStyle = color || active;
ctx.globalAlpha = event.context.velocity ?? event.value?.gain ?? 1; ctx.globalAlpha = event.context.velocity ?? 1;
const timePx = scale((event.whole.begin - (flipTime ? to : from)) / timeExtent, ...timeRange); const timePx = scale((event.whole.begin - (flipTime ? to : from)) / timeExtent, ...timeRange);
let durationPx = scale(event.duration / timeExtent, 0, timeAxis); let durationPx = scale(event.duration / timeExtent, 0, timeAxis);
const value = getValue(event); const value = getValue(event);
@@ -277,14 +267,6 @@ export function pianoroll({
]; ];
} }
isActive ? ctx.strokeRect(...coords) : ctx.fillRect(...coords); isActive ? ctx.strokeRect(...coords) : ctx.fillRect(...coords);
if (labels) {
const label = event.value.note ?? event.value.s + (event.value.n ? `:${event.value.n}` : '');
ctx.font = `${barThickness * 0.75}px monospace`;
ctx.strokeStyle = 'black';
ctx.fillStyle = isActive ? 'white' : 'black';
ctx.textBaseline = 'top';
ctx.fillText(label, ...coords);
}
}); });
ctx.globalAlpha = 1; // reset! ctx.globalAlpha = 1; // reset!
const playheadPosition = scale(-from / timeExtent, ...timeRange); const playheadPosition = scale(-from / timeExtent, ...timeRange);
@@ -302,7 +284,7 @@ export function pianoroll({
return this; return this;
} }
export function getDrawOptions(drawTime, options = {}) { function getOptions(drawTime, options = {}) {
let [lookbehind, lookahead] = drawTime; let [lookbehind, lookahead] = drawTime;
lookbehind = Math.abs(lookbehind); lookbehind = Math.abs(lookbehind);
const cycles = lookahead + lookbehind; const cycles = lookahead + lookbehind;
@@ -311,18 +293,5 @@ export function getDrawOptions(drawTime, options = {}) {
} }
Pattern.prototype.punchcard = function (options) { Pattern.prototype.punchcard = function (options) {
return this.onPaint((ctx, time, haps, drawTime) => return this.onPaint((ctx, time, haps, drawTime) => pianoroll({ ctx, time, haps, ...getOptions(drawTime, options) }));
pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, options) }),
);
}; };
/* Pattern.prototype.pianoroll = function (options) {
return this.onPaint((ctx, time, haps, drawTime) =>
pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, { fold: 0, ...options }) }),
);
}; */
export function drawPianoroll(options) {
const { drawTime, ...rest } = options;
pianoroll({ ...getDrawOptions(drawTime), ...rest });
}
+28 -47
View File
@@ -3,7 +3,6 @@ import { evaluate as _evaluate } from './evaluate.mjs';
import { logger } from './logger.mjs'; import { logger } from './logger.mjs';
import { setTime } from './time.mjs'; import { setTime } from './time.mjs';
import { evalScope } from './evaluate.mjs'; import { evalScope } from './evaluate.mjs';
import { register } from './pattern.mjs';
export function repl({ export function repl({
interval, interval,
@@ -19,26 +18,46 @@ export function repl({
}) { }) {
const scheduler = new Cyclist({ const scheduler = new Cyclist({
interval, interval,
onTrigger: getTrigger({ defaultOutput, getTime }), onTrigger: async (hap, deadline, duration, cps) => {
try {
if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
await defaultOutput(hap, deadline, duration, cps);
}
if (hap.context.onTrigger) {
// call signature of output / onTrigger is different...
await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps);
}
} catch (err) {
logger(`[cyclist] error: ${err.message}`, 'error');
}
},
onError: onSchedulerError, onError: onSchedulerError,
getTime, getTime,
onToggle, onToggle,
}); });
const setPattern = (pattern, autostart = true) => {
pattern = editPattern?.(pattern) || pattern;
scheduler.setPattern(pattern, autostart);
};
setTime(() => scheduler.now()); // TODO: refactor? setTime(() => scheduler.now()); // TODO: refactor?
const evaluate = async (code, autostart = true) => { const evaluate = async (code, autostart = true) => {
if (!code) { if (!code) {
throw new Error('no code to evaluate'); throw new Error('no code to evaluate');
} }
try { try {
await beforeEval?.({ code }); beforeEval?.({ code });
scheduler.setCps(1); // reset cps in case the code does not contain a setCps call
// problem: when the code does contain a setCps after an awaited promise,
// the cps will be 1 until the promise resolves
// example:
/*
await new Promise(resolve => setTimeout(resolve,1000))
setCps(.5)
note("c a f e")
*/
// to make sure the setCps inside the code is called immediately,
// it has to be placed first
let { pattern } = await _evaluate(code, transpiler); let { pattern } = await _evaluate(code, transpiler);
logger(`[eval] code updated`); logger(`[eval] code updated`);
setPattern(pattern, autostart); pattern = editPattern?.(pattern) || pattern;
scheduler.setPattern(pattern, autostart);
afterEval?.({ code, pattern }); afterEval?.({ code, pattern });
return pattern; return pattern;
} catch (err) { } catch (err) {
@@ -51,47 +70,9 @@ export function repl({
const start = () => scheduler.start(); const start = () => scheduler.start();
const pause = () => scheduler.pause(); const pause = () => scheduler.pause();
const setCps = (cps) => scheduler.setCps(cps); const setCps = (cps) => scheduler.setCps(cps);
const setCpm = (cpm) => scheduler.setCps(cpm / 60);
// the following functions use the cps value, which is why they are defined here..
const loopAt = register('loopAt', (cycles, pat) => {
return pat.loopAtCps(cycles, scheduler.cps);
});
const fit = register('fit', (pat) =>
pat.withHap((hap) =>
hap.withValue((v) => ({
...v,
speed: scheduler.cps / hap.whole.duration, // overwrite speed completely?
unit: 'c',
})),
),
);
evalScope({ evalScope({
loopAt,
fit,
setCps, setCps,
setcps: setCps, setcps: setCps,
setCpm,
setcpm: setCpm,
}); });
return { scheduler, evaluate, start, stop, pause, setCps };
return { scheduler, evaluate, start, stop, pause, setCps, setPattern };
} }
export const getTrigger =
({ getTime, defaultOutput }) =>
async (hap, deadline, duration, cps) => {
try {
if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
await defaultOutput(hap, deadline, duration, cps);
}
if (hap.context.onTrigger) {
// call signature of output / onTrigger is different...
await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps);
}
} catch (err) {
logger(`[cyclist] error: ${err.message}`, 'error');
}
};
+1 -1
View File
@@ -27,7 +27,7 @@ export const isaw2 = isaw.toBipolar();
* *
* @return {Pattern} * @return {Pattern}
* @example * @example
* "c3 [eb3,g3] g2 [g3,bb3]".note().clip(saw.slow(4)) * "c3 [eb3,g3] g2 [g3,bb3]".legato(saw.slow(4)).note()
* @example * @example
* saw.range(0,8).segment(8).scale('C major').slow(4).note() * saw.range(0,8).segment(8).scale('C major').slow(4).note()
* *
-117
View File
@@ -1,117 +0,0 @@
import { Pattern } from './index.mjs';
// polar coords -> xy
function fromPolar(angle, radius, cx, cy) {
const radians = ((angle - 90) * Math.PI) / 180;
return [cx + Math.cos(radians) * radius, cy + Math.sin(radians) * radius];
}
const xyOnSpiral = (angle, margin, cx, cy, rotate = 0) => fromPolar((angle + rotate) * 360, margin * angle, cx, cy); // TODO: logSpiral
// draw spiral / segment of spiral
function spiralSegment(options) {
let {
ctx,
from = 0,
to = 3,
margin = 50,
cx = 100,
cy = 100,
rotate = 0,
thickness = margin / 2,
color = '#0000ff30',
cap = 'round',
stretch = 1,
fromOpacity = 1,
toOpacity = 1,
} = options;
from *= stretch;
to *= stretch;
rotate *= stretch;
ctx.lineWidth = thickness;
ctx.lineCap = cap;
ctx.strokeStyle = color;
ctx.globalAlpha = fromOpacity;
ctx.beginPath();
let [sx, sy] = xyOnSpiral(from, margin, cx, cy, rotate);
ctx.moveTo(sx, sy);
const increment = 1 / 60;
let angle = from;
while (angle <= to) {
const [x, y] = xyOnSpiral(angle, margin, cx, cy, rotate);
//ctx.lineWidth = angle*thickness;
ctx.globalAlpha = ((angle - from) / (to - from)) * toOpacity;
ctx.lineTo(x, y);
angle += increment;
}
ctx.stroke();
}
Pattern.prototype.spiral = function (options = {}) {
const {
stretch = 1,
size = 80,
thickness = size / 2,
cap = 'butt', // round butt squar,
inset = 3, // start angl,
playheadColor = '#ffffff90',
playheadLength = 0.02,
playheadThickness = thickness,
padding = 0,
steady = 1,
inactiveColor = '#ffffff20',
colorizeInactive = 0,
fade = true,
// logSpiral = true,
} = options;
function spiral({ ctx, time, haps, drawTime }) {
ctx.clearRect(0, 0, ctx.canvas.clientWidth, ctx.canvas.clientHeight);
const [cx, cy] = [ctx.canvas.width / 2, ctx.canvas.height / 2];
const settings = {
margin: size / stretch,
cx,
cy,
stretch,
cap,
thickness,
};
const playhead = {
...settings,
thickness: playheadThickness,
from: inset - playheadLength,
to: inset,
color: playheadColor,
};
const [min] = drawTime;
const rotate = steady * time;
haps.forEach((hap) => {
const isActive = hap.whole.begin <= time && hap.endClipped > time;
const from = hap.whole.begin - time + inset;
const to = hap.endClipped - time + inset - padding;
const { color } = hap.context;
const opacity = fade ? 1 - Math.abs((hap.whole.begin - time) / min) : 1;
spiralSegment({
ctx,
...settings,
from,
to,
rotate,
color: colorizeInactive || isActive ? color : inactiveColor,
fromOpacity: opacity,
toOpacity: opacity,
});
});
spiralSegment({
ctx,
...playhead,
rotate,
});
}
return this.onPaint((ctx, time, haps, drawTime) => spiral({ ctx, time, haps, drawTime }));
};
-40
View File
@@ -1,40 +0,0 @@
/*test for issue 302 support alternative solmization types */
import { sol2note } from '../util.mjs';
import { test } from 'vitest';
import assert from 'assert';
test('solmization - letters', () => {
const result = sol2note(60, 'letters');
const expected = 'C4';
assert.equal(result, expected);
});
test('solmization - solfeggio', () => {
const result = sol2note(60, 'solfeggio');
const expected = 'Do4';
assert.equal(result, expected);
});
test('solmization - indian', () => {
const result = sol2note(60, 'indian');
const expected = 'Sa4';
assert.equal(result, expected);
});
test('solmization - german', () => {
const result = sol2note(60, 'german');
const expected = 'C4';
assert.equal(result, expected);
});
test('solmization - byzantine', () => {
const result = sol2note(60, 'byzantine');
const expected = 'Ni4';
assert.equal(result, expected);
});
test('solmization - japanese', () => {
const result = sol2note(60, 'japanese');
const expected = 'I4';
assert.equal(result, expected);
});
+17 -17
View File
@@ -8,8 +8,8 @@ import { pure } from '../pattern.mjs';
import { import {
isNote, isNote,
tokenizeNote, tokenizeNote,
noteToMidi, toMidi,
midiToFreq, fromMidi,
freqToMidi, freqToMidi,
_mod, _mod,
compose, compose,
@@ -75,27 +75,27 @@ describe('isNote', () => {
expect(tokenizeNote(123)).toStrictEqual([]); expect(tokenizeNote(123)).toStrictEqual([]);
}); });
}); });
describe('noteToMidi', () => { describe('toMidi', () => {
it('should turn notes into midi', () => { it('should turn notes into midi', () => {
expect(noteToMidi('A4')).toEqual(69); expect(toMidi('A4')).toEqual(69);
expect(noteToMidi('C4')).toEqual(60); expect(toMidi('C4')).toEqual(60);
expect(noteToMidi('Db4')).toEqual(61); expect(toMidi('Db4')).toEqual(61);
expect(noteToMidi('C3')).toEqual(48); expect(toMidi('C3')).toEqual(48);
expect(noteToMidi('Cb3')).toEqual(47); expect(toMidi('Cb3')).toEqual(47);
expect(noteToMidi('Cbb3')).toEqual(46); expect(toMidi('Cbb3')).toEqual(46);
expect(noteToMidi('C#3')).toEqual(49); expect(toMidi('C#3')).toEqual(49);
expect(noteToMidi('C#3')).toEqual(49); expect(toMidi('C#3')).toEqual(49);
expect(noteToMidi('C##3')).toEqual(50); expect(toMidi('C##3')).toEqual(50);
}); });
it('should throw an error when given a non-note', () => { it('should throw an error when given a non-note', () => {
expect(() => noteToMidi('Q')).toThrowError(`not a note: "Q"`); expect(() => toMidi('Q')).toThrowError(`not a note: "Q"`);
expect(() => noteToMidi('Z')).toThrowError(`not a note: "Z"`); expect(() => toMidi('Z')).toThrowError(`not a note: "Z"`);
}); });
}); });
describe('midiToFreq', () => { describe('fromMidi', () => {
it('should turn midi into frequency', () => { it('should turn midi into frequency', () => {
expect(midiToFreq(69)).toEqual(440); expect(fromMidi(69)).toEqual(440);
expect(midiToFreq(57)).toEqual(220); expect(fromMidi(57)).toEqual(220);
}); });
}); });
describe('freqToMidi', () => { describe('freqToMidi', () => {
+14 -73
View File
@@ -6,12 +6,12 @@ This program is free software: you can redistribute it and/or modify it under th
// returns true if the given string is a note // returns true if the given string is a note
export const isNoteWithOctave = (name) => /^[a-gA-G][#bs]*[0-9]$/.test(name); export const isNoteWithOctave = (name) => /^[a-gA-G][#bs]*[0-9]$/.test(name);
export const isNote = (name) => /^[a-gA-G][#bsf]*[0-9]?$/.test(name); export const isNote = (name) => /^[a-gA-G][#bs]*[0-9]?$/.test(name);
export const tokenizeNote = (note) => { export const tokenizeNote = (note) => {
if (typeof note !== 'string') { if (typeof note !== 'string') {
return []; return [];
} }
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)([0-9])?$/)?.slice(1) || []; const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bs]*)([0-9])?$/)?.slice(1) || [];
if (!pc) { if (!pc) {
return []; return [];
} }
@@ -19,16 +19,16 @@ export const tokenizeNote = (note) => {
}; };
// turns the given note into its midi number representation // turns the given note into its midi number representation
export const noteToMidi = (note) => { export const toMidi = (note) => {
const [pc, acc, oct = 3] = tokenizeNote(note); const [pc, acc, oct = 3] = tokenizeNote(note);
if (!pc) { if (!pc) {
throw new Error('not a note: "' + note + '"'); throw new Error('not a note: "' + note + '"');
} }
const chroma = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 }[pc.toLowerCase()]; const chroma = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 }[pc.toLowerCase()];
const offset = acc?.split('').reduce((o, char) => o + { '#': 1, b: -1, s: 1, f: -1 }[char], 0) || 0; const offset = acc?.split('').reduce((o, char) => o + { '#': 1, b: -1, s: 1 }[char], 0) || 0;
return (Number(oct) + 1) * 12 + chroma + offset; return (Number(oct) + 1) * 12 + chroma + offset;
}; };
export const midiToFreq = (n) => { export const fromMidi = (n) => {
return Math.pow(2, (n - 69) / 12) * 440; return Math.pow(2, (n - 69) / 12) * 440;
}; };
@@ -45,7 +45,7 @@ export const valueToMidi = (value, fallbackValue) => {
return freqToMidi(freq); return freqToMidi(freq);
} }
if (typeof note === 'string') { if (typeof note === 'string') {
return noteToMidi(note); return toMidi(note);
} }
if (typeof note === 'number') { if (typeof note === 'number') {
return note; return note;
@@ -62,19 +62,18 @@ export const valueToMidi = (value, fallbackValue) => {
*/ */
export const getFreq = (noteOrMidi) => { export const getFreq = (noteOrMidi) => {
if (typeof noteOrMidi === 'number') { if (typeof noteOrMidi === 'number') {
return midiToFreq(noteOrMidi); return fromMidi(noteOrMidi);
} }
return midiToFreq(noteToMidi(noteOrMidi)); return fromMidi(toMidi(noteOrMidi));
}; };
const pcs = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'];
/** /**
* @deprecated only used in workshop (first-notes) * @deprecated does not appear to be referenced or invoked anywhere in the codebase
* @noAutocomplete * @noAutocomplete
*/ */
export const midi2note = (n) => { export const midi2note = (n) => {
const oct = Math.floor(n / 12) - 1; const oct = Math.floor(n / 12) - 1;
const pc = pcs[n % 12]; const pc = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'][n % 12];
return pc + oct; return pc + oct;
}; };
@@ -92,7 +91,7 @@ export const getPlayableNoteValue = (hap) => {
} }
// if value is number => interpret as midi number as long as its not marked as frequency // if value is number => interpret as midi number as long as its not marked as frequency
if (typeof note === 'number' && context.type !== 'frequency') { if (typeof note === 'number' && context.type !== 'frequency') {
note = midiToFreq(hap.value); note = fromMidi(hap.value);
} else if (typeof note === 'number' && context.type === 'frequency') { } else if (typeof note === 'number' && context.type === 'frequency') {
note = hap.value; // legacy workaround.. will be removed in the future note = hap.value; // legacy workaround.. will be removed in the future
} else if (typeof note !== 'string' || !isNote(note)) { } else if (typeof note !== 'string' || !isNote(note)) {
@@ -111,9 +110,9 @@ export const getFrequency = (hap) => {
return getFreq(value.note || value.n || value.value); return getFreq(value.note || value.n || value.value);
} }
if (typeof value === 'number' && context.type !== 'frequency') { if (typeof value === 'number' && context.type !== 'frequency') {
value = midiToFreq(hap.value); value = fromMidi(hap.value);
} else if (typeof value === 'string' && isNote(value)) { } else if (typeof value === 'string' && isNote(value)) {
value = midiToFreq(noteToMidi(hap.value)); value = fromMidi(toMidi(hap.value));
} else if (typeof value !== 'number') { } else if (typeof value !== 'number') {
throw new Error('not a note or frequency: ' + value); throw new Error('not a note or frequency: ' + value);
} }
@@ -171,7 +170,7 @@ export function parseNumeral(numOrString) {
return asNumber; return asNumber;
} }
if (isNote(numOrString)) { if (isNote(numOrString)) {
return noteToMidi(numOrString); return toMidi(numOrString);
} }
throw new Error(`cannot parse as numeral: "${numOrString}"`); throw new Error(`cannot parse as numeral: "${numOrString}"`);
} }
@@ -213,61 +212,3 @@ export const splitAt = function (index, value) {
}; };
export const zipWith = (f, xs, ys) => xs.map((n, i) => f(n, ys[i])); export const zipWith = (f, xs, ys) => xs.map((n, i) => f(n, ys[i]));
export const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
/* solmization, not used yet */
const solfeggio = ['Do', 'Reb', 'Re', 'Mib', 'Mi', 'Fa', 'Solb', 'Sol', 'Lab', 'La', 'Sib', 'Si']; /*solffegio notes*/
const indian = [
'Sa',
'Re',
'Ga',
'Ma',
'Pa',
'Dha',
'Ni',
]; /*indian musical notes, seems like they do not use flats or sharps*/
const german = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Hb', 'H']; /*german & dutch musical notes*/
const byzantine = [
'Ni',
'Pab',
'Pa',
'Voub',
'Vou',
'Ga',
'Dib',
'Di',
'Keb',
'Ke',
'Zob',
'Zo',
]; /*byzantine musical notes*/
const japanese = [
'I',
'Ro',
'Ha',
'Ni',
'Ho',
'He',
'To',
]; /*traditional japanese musical notes, seems like they do not use falts or sharps*/
const english = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'];
export const sol2note = (n, notation = 'letters') => {
const pc =
notation === 'solfeggio'
? solfeggio /*check if its is any of the following*/
: notation === 'indian'
? indian
: notation === 'german'
? german
: notation === 'byzantine'
? byzantine
: notation === 'japanese'
? japanese
: english; /*if not use standard version*/
const note = pc[n % 12]; /*calculating the midi value to the note*/
const oct = Math.floor(n / 12) - 1;
return note + oct;
};
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel.cycles/csound", "name": "@strudel.cycles/csound",
"version": "0.8.0", "version": "0.6.2",
"description": "csound bindings for strudel", "description": "csound bindings for strudel",
"main": "index.mjs", "main": "index.mjs",
"publishConfig": { "publishConfig": {
@@ -32,11 +32,11 @@
}, },
"homepage": "https://github.com/tidalcycles/strudel#readme", "homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": { "dependencies": {
"@csound/browser": "6.18.7", "@csound/browser": "6.18.5",
"@strudel.cycles/core": "workspace:*", "@strudel.cycles/core": "workspace:*",
"@strudel.cycles/webaudio": "workspace:*" "@strudel.cycles/webaudio": "workspace:*"
}, },
"devDependencies": { "devDependencies": {
"vite": "^4.3.3" "vite": "^3.2.2"
} }
} }
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel.cycles/eval", "name": "@strudel.cycles/eval",
"version": "0.8.0", "version": "0.6.2",
"description": "Code evaluator for strudel", "description": "Code evaluator for strudel",
"main": "index.mjs", "main": "index.mjs",
"publishConfig": { "publishConfig": {
@@ -44,7 +44,7 @@
}, },
"devDependencies": { "devDependencies": {
"@strudel.cycles/mini": "workspace:*", "@strudel.cycles/mini": "workspace:*",
"vite": "^4.3.3", "vite": "^3.2.2",
"vitest": "^0.28.0" "vitest": "^0.25.7"
} }
} }
+3 -3
View File
@@ -7,7 +7,7 @@ This program is free software: you can redistribute it and/or modify it under th
import * as _WebMidi from 'webmidi'; import * as _WebMidi from 'webmidi';
import { Pattern, isPattern, logger } from '@strudel.cycles/core'; import { Pattern, isPattern, logger } from '@strudel.cycles/core';
import { getAudioContext } from '@strudel.cycles/webaudio'; import { getAudioContext } from '@strudel.cycles/webaudio';
import { noteToMidi } from '@strudel.cycles/core'; import { toMidi } from '@strudel.cycles/core';
// if you use WebMidi from outside of this package, make sure to import that instance: // if you use WebMidi from outside of this package, make sure to import that instance:
export const { WebMidi } = _WebMidi; export const { WebMidi } = _WebMidi;
@@ -105,7 +105,7 @@ Pattern.prototype.midi = function (output) {
hap.ensureObjectValue(); hap.ensureObjectValue();
// calculate time // calculate time
const timingOffset = WebMidi.time - getAudioContext().getOutputTimestamp().contextTime * 1000; const timingOffset = WebMidi.time - getAudioContext().currentTime * 1000;
time = time * 1000 + timingOffset; time = time * 1000 + timingOffset;
// destructure value // destructure value
@@ -114,7 +114,7 @@ Pattern.prototype.midi = function (output) {
const duration = hap.duration.valueOf() * 1000 - 5; const duration = hap.duration.valueOf() * 1000 - 5;
if (note) { if (note) {
const midiNumber = noteToMidi(note); const midiNumber = toMidi(note);
device.playNote(midiNumber, midichan, { device.playNote(midiNumber, midichan, {
time, time,
duration, duration,
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel.cycles/midi", "name": "@strudel.cycles/midi",
"version": "0.8.0", "version": "0.6.0",
"description": "Midi API for strudel", "description": "Midi API for strudel",
"main": "index.mjs", "main": "index.mjs",
"publishConfig": { "publishConfig": {
@@ -31,9 +31,9 @@
"dependencies": { "dependencies": {
"@strudel.cycles/core": "workspace:*", "@strudel.cycles/core": "workspace:*",
"@strudel.cycles/webaudio": "workspace:*", "@strudel.cycles/webaudio": "workspace:*",
"webmidi": "^3.1.5" "webmidi": "^3.0.21"
}, },
"devDependencies": { "devDependencies": {
"vite": "^4.3.3" "vite": "^3.2.2"
} }
} }
+232 -169
View File
@@ -1,4 +1,4 @@
// Generated by Peggy 3.0.2. // Generated by Peggy 2.0.1.
// //
// https://peggyjs.org/ // https://peggyjs.org/
@@ -43,19 +43,16 @@ peg$SyntaxError.prototype.format = function(sources) {
} }
} }
var s = this.location.start; var s = this.location.start;
var offset_s = (this.location.source && (typeof this.location.source.offset === "function")) var loc = this.location.source + ":" + s.line + ":" + s.column;
? this.location.source.offset(s)
: s;
var loc = this.location.source + ":" + offset_s.line + ":" + offset_s.column;
if (src) { if (src) {
var e = this.location.end; var e = this.location.end;
var filler = peg$padEnd("", offset_s.line.toString().length, ' '); var filler = peg$padEnd("", s.line.toString().length, ' ');
var line = src[s.line - 1]; var line = src[s.line - 1];
var last = s.line === e.line ? e.column : line.length + 1; var last = s.line === e.line ? e.column : line.length + 1;
var hatLen = (last - s.column) || 1; var hatLen = (last - s.column) || 1;
str += "\n --> " + loc + "\n" str += "\n --> " + loc + "\n"
+ filler + " |\n" + filler + " |\n"
+ offset_s.line + " | " + line + "\n" + s.line + " | " + line + "\n"
+ filler + " | " + peg$padEnd("", s.column - 1, ' ') + filler + " | " + peg$padEnd("", s.column - 1, ' ')
+ peg$padEnd("", hatLen, "^"); + peg$padEnd("", hatLen, "^");
} else { } else {
@@ -183,8 +180,8 @@ function peg$parse(input, options) {
var peg$c6 = "\""; var peg$c6 = "\"";
var peg$c7 = "'"; var peg$c7 = "'";
var peg$c8 = "#"; var peg$c8 = "#";
var peg$c9 = "^"; var peg$c9 = "_";
var peg$c10 = "_"; var peg$c10 = "^";
var peg$c11 = "["; var peg$c11 = "[";
var peg$c12 = "]"; var peg$c12 = "]";
var peg$c13 = "{"; var peg$c13 = "{";
@@ -220,7 +217,9 @@ function peg$parse(input, options) {
var peg$r2 = /^[0-9]/; var peg$r2 = /^[0-9]/;
var peg$r3 = /^[ \n\r\t]/; var peg$r3 = /^[ \n\r\t]/;
var peg$r4 = /^[0-9a-zA-Z~]/; var peg$r4 = /^[0-9a-zA-Z~]/;
var peg$r5 = /^[^\n]/; var peg$r5 = /^[A-Za-z]/;
var peg$r6 = /^[A-Za-z0-9_]/;
var peg$r7 = /^[^\n]/;
var peg$e0 = peg$otherExpectation("number"); var peg$e0 = peg$otherExpectation("number");
var peg$e1 = peg$literalExpectation(".", false); var peg$e1 = peg$literalExpectation(".", false);
@@ -238,81 +237,84 @@ function peg$parse(input, options) {
var peg$e13 = peg$literalExpectation("'", false); var peg$e13 = peg$literalExpectation("'", false);
var peg$e14 = peg$classExpectation([["0", "9"], ["a", "z"], ["A", "Z"], "~"], false, false); var peg$e14 = peg$classExpectation([["0", "9"], ["a", "z"], ["A", "Z"], "~"], false, false);
var peg$e15 = peg$literalExpectation("#", false); var peg$e15 = peg$literalExpectation("#", false);
var peg$e16 = peg$literalExpectation("^", false); var peg$e16 = peg$literalExpectation("_", false);
var peg$e17 = peg$literalExpectation("_", false); var peg$e17 = peg$literalExpectation("^", false);
var peg$e18 = peg$literalExpectation("[", false); var peg$e18 = peg$classExpectation([["A", "Z"], ["a", "z"]], false, false);
var peg$e19 = peg$literalExpectation("]", false); var peg$e19 = peg$classExpectation([["A", "Z"], ["a", "z"], ["0", "9"], "_"], false, false);
var peg$e20 = peg$literalExpectation("{", false); var peg$e20 = peg$literalExpectation("[", false);
var peg$e21 = peg$literalExpectation("}", false); var peg$e21 = peg$literalExpectation("]", false);
var peg$e22 = peg$literalExpectation("%", false); var peg$e22 = peg$literalExpectation("{", false);
var peg$e23 = peg$literalExpectation("<", false); var peg$e23 = peg$literalExpectation("}", false);
var peg$e24 = peg$literalExpectation(">", false); var peg$e24 = peg$literalExpectation("%", false);
var peg$e25 = peg$literalExpectation("@", false); var peg$e25 = peg$literalExpectation("<", false);
var peg$e26 = peg$literalExpectation("!", false); var peg$e26 = peg$literalExpectation(">", false);
var peg$e27 = peg$literalExpectation("(", false); var peg$e27 = peg$literalExpectation("@", false);
var peg$e28 = peg$literalExpectation(")", false); var peg$e28 = peg$literalExpectation("!", false);
var peg$e29 = peg$literalExpectation("/", false); var peg$e29 = peg$literalExpectation("(", false);
var peg$e30 = peg$literalExpectation("*", false); var peg$e30 = peg$literalExpectation(")", false);
var peg$e31 = peg$literalExpectation("?", false); var peg$e31 = peg$literalExpectation("/", false);
var peg$e32 = peg$literalExpectation(":", false); var peg$e32 = peg$literalExpectation("*", false);
var peg$e33 = peg$literalExpectation("struct", false); var peg$e33 = peg$literalExpectation("?", false);
var peg$e34 = peg$literalExpectation("target", false); var peg$e34 = peg$literalExpectation(":", false);
var peg$e35 = peg$literalExpectation("euclid", false); var peg$e35 = peg$literalExpectation("struct", false);
var peg$e36 = peg$literalExpectation("slow", false); var peg$e36 = peg$literalExpectation("target", false);
var peg$e37 = peg$literalExpectation("rotL", false); var peg$e37 = peg$literalExpectation("euclid", false);
var peg$e38 = peg$literalExpectation("rotR", false); var peg$e38 = peg$literalExpectation("slow", false);
var peg$e39 = peg$literalExpectation("fast", false); var peg$e39 = peg$literalExpectation("rotL", false);
var peg$e40 = peg$literalExpectation("scale", false); var peg$e40 = peg$literalExpectation("rotR", false);
var peg$e41 = peg$literalExpectation("//", false); var peg$e41 = peg$literalExpectation("fast", false);
var peg$e42 = peg$classExpectation(["\n"], true, false); var peg$e42 = peg$literalExpectation("scale", false);
var peg$e43 = peg$literalExpectation("cat", false); var peg$e43 = peg$literalExpectation("//", false);
var peg$e44 = peg$literalExpectation("$", false); var peg$e44 = peg$classExpectation(["\n"], true, false);
var peg$e45 = peg$literalExpectation("setcps", false); var peg$e45 = peg$literalExpectation("cat", false);
var peg$e46 = peg$literalExpectation("setbpm", false); var peg$e46 = peg$literalExpectation("$", false);
var peg$e47 = peg$literalExpectation("hush", false); var peg$e47 = peg$literalExpectation("setcps", false);
var peg$e48 = peg$literalExpectation("setbpm", false);
var peg$e49 = peg$literalExpectation("hush", false);
var peg$f0 = function() { return parseFloat(text()); }; var peg$f0 = function() { return parseFloat(text()); };
var peg$f1 = function(chars) { return new AtomStub(chars.join("")) }; var peg$f1 = function(chars) { return new AtomStub(chars.join("")) };
var peg$f2 = function(s) { return s }; var peg$f2 = function(h, t) { return new MemoryStub(h + t.join("")) };
var peg$f3 = function(s, stepsPerCycle) { s.arguments_.stepsPerCycle = stepsPerCycle ; return s; }; var peg$f3 = function(s) { return s };
var peg$f4 = function(a) { return a }; var peg$f4 = function(s, stepsPerCycle) { s.arguments_.stepsPerCycle = stepsPerCycle ; return s; };
var peg$f5 = function(s) { s.arguments_.alignment = 'slowcat'; return s; }; var peg$f5 = function(a) { return a };
var peg$f6 = function(a) { return x => x.options_['weight'] = a }; var peg$f6 = function(s) { s.arguments_.alignment = 'slowcat'; return s; };
var peg$f7 = function(a) { return x => x.options_['reps'] = a }; var peg$f7 = function(a) { return x => x.options_['weight'] = a };
var peg$f8 = function(p, s, r) { return x => x.options_['ops'].push({ type_: "bjorklund", arguments_ :{ pulse: p, step:s, rotation:r }}) }; var peg$f8 = function(a) { return x => x.options_['reps'] = a };
var peg$f9 = function(a) { return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'slow' }}) }; var peg$f9 = function(p, s, r) { return x => x.options_['ops'].push({ type_: "bjorklund", arguments_ :{ pulse: p, step:s, rotation:r }}) };
var peg$f10 = function(a) { return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'fast' }}) }; var peg$f10 = function(a) { return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'slow' }}) };
var peg$f11 = function(a) { return x => x.options_['ops'].push({ type_: "degradeBy", arguments_ :{ amount:a, seed: seed++ } }) }; var peg$f11 = function(a) { return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'fast' }}) };
var peg$f12 = function(s) { return x => x.options_['ops'].push({ type_: "tail", arguments_ :{ element:s } }) }; var peg$f12 = function(a) { return x => x.options_['ops'].push({ type_: "degradeBy", arguments_ :{ amount:a } }) };
var peg$f13 = function(s, ops) { const result = new ElementStub(s, {ops: [], weight: 1, reps: 1}); var peg$f13 = function(s) { return x => x.options_['ops'].push({ type_: "tail", arguments_ :{ element:s } }) };
var peg$f14 = function(s, ops) { const result = new ElementStub(s, {ops: [], weight: 1, reps: 1});
for (const op of ops) { for (const op of ops) {
op(result); op(result);
} }
return result; return result;
}; };
var peg$f14 = function(s) { return new PatternStub(s, 'fastcat'); }; var peg$f15 = function(s) { return new PatternStub(s, 'fastcat'); };
var peg$f15 = function(tail) { return { alignment: 'stack', list: tail }; }; var peg$f16 = function(tail) { return { alignment: 'stack', list: tail }; };
var peg$f16 = function(tail) { return { alignment: 'rand', list: tail, seed: seed++ }; }; var peg$f17 = function(tail) { return { alignment: 'rand', list: tail }; };
var peg$f17 = function(head, tail) { if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment, tail.seed); } else { return head; } }; var peg$f18 = function(head, tail) { if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment); } else { return head; } };
var peg$f18 = function(head, tail) { return new PatternStub(tail ? [head, ...tail.list] : [head], 'polymeter'); }; var peg$f19 = function(head, tail) { return new PatternStub(tail ? [head, ...tail.list] : [head], 'polymeter'); };
var peg$f19 = function(sc) { return sc; }; var peg$f20 = function(sc) { return sc; };
var peg$f20 = function(s) { return { name: "struct", args: { mini:s }}}; var peg$f21 = function(s) { return { name: "struct", args: { mini:s }}};
var peg$f21 = function(s) { return { name: "target", args : { name:s}}}; var peg$f22 = function(s) { return { name: "target", args : { name:s}}};
var peg$f22 = function(p, s, r) { return { name: "bjorklund", args :{ pulse: p, step:parseInt(s) }}}; var peg$f23 = function(p, s, r) { return { name: "bjorklund", args :{ pulse: p, step:parseInt(s) }}};
var peg$f23 = function(a) { return { name: "stretch", args :{ amount: a}}}; var peg$f24 = function(a) { return { name: "stretch", args :{ amount: a}}};
var peg$f24 = function(a) { return { name: "shift", args :{ amount: "-"+a}}}; var peg$f25 = function(a) { return { name: "shift", args :{ amount: "-"+a}}};
var peg$f25 = function(a) { return { name: "shift", args :{ amount: a}}}; var peg$f26 = function(a) { return { name: "shift", args :{ amount: a}}};
var peg$f26 = function(a) { return { name: "stretch", args :{ amount: "1/"+a}}}; var peg$f27 = function(a) { return { name: "stretch", args :{ amount: "1/"+a}}};
var peg$f27 = function(s) { return { name: "scale", args :{ scale: s.join("")}}}; var peg$f28 = function(s) { return { name: "scale", args :{ scale: s.join("")}}};
var peg$f28 = function(s, v) { return v}; var peg$f29 = function(s, v) { return v};
var peg$f29 = function(s, ss) { ss.unshift(s); return new PatternStub(ss, 'slowcat'); }; var peg$f30 = function(s, ss) { ss.unshift(s); return new PatternStub(ss, 'slowcat'); };
var peg$f30 = function(sg) {return sg}; var peg$f31 = function(sg) {return sg};
var peg$f31 = function(o, soc) { return new OperatorStub(o.name,o.args,soc)}; var peg$f32 = function(o, soc) { return new OperatorStub(o.name,o.args,soc)};
var peg$f32 = function(sc) { return sc }; var peg$f33 = function(sc) { return sc };
var peg$f33 = function(c) { return c }; var peg$f34 = function(c) { return c };
var peg$f34 = function(v) { return new CommandStub("setcps", { value: v})}; var peg$f35 = function(v) { return new CommandStub("setcps", { value: v})};
var peg$f35 = function(v) { return new CommandStub("setcps", { value: (v/120/2)})}; var peg$f36 = function(v) { return new CommandStub("setcps", { value: (v/120/2)})};
var peg$f36 = function() { return new CommandStub("hush")}; var peg$f37 = function() { return new CommandStub("hush")};
var peg$currPos = 0; var peg$currPos = 0;
var peg$savedPos = 0; var peg$savedPos = 0;
var peg$posDetailsCache = [{ line: 1, column: 1 }]; var peg$posDetailsCache = [{ line: 1, column: 1 }];
@@ -425,11 +427,11 @@ function peg$parse(input, options) {
} }
} }
function peg$computeLocation(startPos, endPos, offset) { function peg$computeLocation(startPos, endPos) {
var startPosDetails = peg$computePosDetails(startPos); var startPosDetails = peg$computePosDetails(startPos);
var endPosDetails = peg$computePosDetails(endPos); var endPosDetails = peg$computePosDetails(endPos);
var res = { return {
source: peg$source, source: peg$source,
start: { start: {
offset: startPos, offset: startPos,
@@ -442,11 +444,6 @@ function peg$parse(input, options) {
column: endPosDetails.column column: endPosDetails.column
} }
}; };
if (offset && peg$source && (typeof peg$source.offset === "function")) {
res.start = peg$source.offset(res.start);
res.end = peg$source.offset(res.end);
}
return res;
} }
function peg$fail(expected) { function peg$fail(expected) {
@@ -842,7 +839,7 @@ function peg$parse(input, options) {
if (peg$silentFails === 0) { peg$fail(peg$e1); } if (peg$silentFails === 0) { peg$fail(peg$e1); }
} }
if (s0 === peg$FAILED) { if (s0 === peg$FAILED) {
if (input.charCodeAt(peg$currPos) === 94) { if (input.charCodeAt(peg$currPos) === 95) {
s0 = peg$c9; s0 = peg$c9;
peg$currPos++; peg$currPos++;
} else { } else {
@@ -850,7 +847,7 @@ function peg$parse(input, options) {
if (peg$silentFails === 0) { peg$fail(peg$e16); } if (peg$silentFails === 0) { peg$fail(peg$e16); }
} }
if (s0 === peg$FAILED) { if (s0 === peg$FAILED) {
if (input.charCodeAt(peg$currPos) === 95) { if (input.charCodeAt(peg$currPos) === 94) {
s0 = peg$c10; s0 = peg$c10;
peg$currPos++; peg$currPos++;
} else { } else {
@@ -893,6 +890,68 @@ function peg$parse(input, options) {
return s0; return s0;
} }
function peg$parsememory() {
var s0, s1, s2, s3, s4, s5;
s0 = peg$currPos;
s1 = peg$parsews();
if (input.charCodeAt(peg$currPos) === 94) {
s2 = peg$c10;
peg$currPos++;
} else {
s2 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e17); }
}
if (s2 !== peg$FAILED) {
if (peg$r5.test(input.charAt(peg$currPos))) {
s3 = input.charAt(peg$currPos);
peg$currPos++;
} else {
s3 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e18); }
}
if (s3 !== peg$FAILED) {
s4 = [];
if (peg$r6.test(input.charAt(peg$currPos))) {
s5 = input.charAt(peg$currPos);
peg$currPos++;
} else {
s5 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e19); }
}
if (s5 !== peg$FAILED) {
while (s5 !== peg$FAILED) {
s4.push(s5);
if (peg$r6.test(input.charAt(peg$currPos))) {
s5 = input.charAt(peg$currPos);
peg$currPos++;
} else {
s5 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e19); }
}
}
} else {
s4 = peg$FAILED;
}
if (s4 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f2(s3, s4);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
return s0;
}
function peg$parsesub_cycle() { function peg$parsesub_cycle() {
var s0, s1, s2, s3, s4, s5, s6, s7; var s0, s1, s2, s3, s4, s5, s6, s7;
@@ -903,7 +962,7 @@ function peg$parse(input, options) {
peg$currPos++; peg$currPos++;
} else { } else {
s2 = peg$FAILED; s2 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e18); } if (peg$silentFails === 0) { peg$fail(peg$e20); }
} }
if (s2 !== peg$FAILED) { if (s2 !== peg$FAILED) {
s3 = peg$parsews(); s3 = peg$parsews();
@@ -915,12 +974,12 @@ function peg$parse(input, options) {
peg$currPos++; peg$currPos++;
} else { } else {
s6 = peg$FAILED; s6 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e19); } if (peg$silentFails === 0) { peg$fail(peg$e21); }
} }
if (s6 !== peg$FAILED) { if (s6 !== peg$FAILED) {
s7 = peg$parsews(); s7 = peg$parsews();
peg$savedPos = s0; peg$savedPos = s0;
s0 = peg$f2(s4); s0 = peg$f3(s4);
} else { } else {
peg$currPos = s0; peg$currPos = s0;
s0 = peg$FAILED; s0 = peg$FAILED;
@@ -947,7 +1006,7 @@ function peg$parse(input, options) {
peg$currPos++; peg$currPos++;
} else { } else {
s2 = peg$FAILED; s2 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e20); } if (peg$silentFails === 0) { peg$fail(peg$e22); }
} }
if (s2 !== peg$FAILED) { if (s2 !== peg$FAILED) {
s3 = peg$parsews(); s3 = peg$parsews();
@@ -959,7 +1018,7 @@ function peg$parse(input, options) {
peg$currPos++; peg$currPos++;
} else { } else {
s6 = peg$FAILED; s6 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e21); } if (peg$silentFails === 0) { peg$fail(peg$e23); }
} }
if (s6 !== peg$FAILED) { if (s6 !== peg$FAILED) {
s7 = peg$parsepolymeter_steps(); s7 = peg$parsepolymeter_steps();
@@ -968,7 +1027,7 @@ function peg$parse(input, options) {
} }
s8 = peg$parsews(); s8 = peg$parsews();
peg$savedPos = s0; peg$savedPos = s0;
s0 = peg$f3(s4, s7); s0 = peg$f4(s4, s7);
} else { } else {
peg$currPos = s0; peg$currPos = s0;
s0 = peg$FAILED; s0 = peg$FAILED;
@@ -994,13 +1053,13 @@ function peg$parse(input, options) {
peg$currPos++; peg$currPos++;
} else { } else {
s1 = peg$FAILED; s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e22); } if (peg$silentFails === 0) { peg$fail(peg$e24); }
} }
if (s1 !== peg$FAILED) { if (s1 !== peg$FAILED) {
s2 = peg$parseslice(); s2 = peg$parseslice();
if (s2 !== peg$FAILED) { if (s2 !== peg$FAILED) {
peg$savedPos = s0; peg$savedPos = s0;
s0 = peg$f4(s2); s0 = peg$f5(s2);
} else { } else {
peg$currPos = s0; peg$currPos = s0;
s0 = peg$FAILED; s0 = peg$FAILED;
@@ -1023,7 +1082,7 @@ function peg$parse(input, options) {
peg$currPos++; peg$currPos++;
} else { } else {
s2 = peg$FAILED; s2 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e23); } if (peg$silentFails === 0) { peg$fail(peg$e25); }
} }
if (s2 !== peg$FAILED) { if (s2 !== peg$FAILED) {
s3 = peg$parsews(); s3 = peg$parsews();
@@ -1035,12 +1094,12 @@ function peg$parse(input, options) {
peg$currPos++; peg$currPos++;
} else { } else {
s6 = peg$FAILED; s6 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e24); } if (peg$silentFails === 0) { peg$fail(peg$e26); }
} }
if (s6 !== peg$FAILED) { if (s6 !== peg$FAILED) {
s7 = peg$parsews(); s7 = peg$parsews();
peg$savedPos = s0; peg$savedPos = s0;
s0 = peg$f5(s4); s0 = peg$f6(s4);
} else { } else {
peg$currPos = s0; peg$currPos = s0;
s0 = peg$FAILED; s0 = peg$FAILED;
@@ -1060,6 +1119,8 @@ function peg$parse(input, options) {
function peg$parseslice() { function peg$parseslice() {
var s0; var s0;
s0 = peg$parsememory();
if (s0 === peg$FAILED) {
s0 = peg$parsestep(); s0 = peg$parsestep();
if (s0 === peg$FAILED) { if (s0 === peg$FAILED) {
s0 = peg$parsesub_cycle(); s0 = peg$parsesub_cycle();
@@ -1070,6 +1131,7 @@ function peg$parse(input, options) {
} }
} }
} }
}
return s0; return s0;
} }
@@ -1109,13 +1171,13 @@ function peg$parse(input, options) {
peg$currPos++; peg$currPos++;
} else { } else {
s1 = peg$FAILED; s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e25); } if (peg$silentFails === 0) { peg$fail(peg$e27); }
} }
if (s1 !== peg$FAILED) { if (s1 !== peg$FAILED) {
s2 = peg$parsenumber(); s2 = peg$parsenumber();
if (s2 !== peg$FAILED) { if (s2 !== peg$FAILED) {
peg$savedPos = s0; peg$savedPos = s0;
s0 = peg$f6(s2); s0 = peg$f7(s2);
} else { } else {
peg$currPos = s0; peg$currPos = s0;
s0 = peg$FAILED; s0 = peg$FAILED;
@@ -1137,13 +1199,13 @@ function peg$parse(input, options) {
peg$currPos++; peg$currPos++;
} else { } else {
s1 = peg$FAILED; s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e26); } if (peg$silentFails === 0) { peg$fail(peg$e28); }
} }
if (s1 !== peg$FAILED) { if (s1 !== peg$FAILED) {
s2 = peg$parsenumber(); s2 = peg$parsenumber();
if (s2 !== peg$FAILED) { if (s2 !== peg$FAILED) {
peg$savedPos = s0; peg$savedPos = s0;
s0 = peg$f7(s2); s0 = peg$f8(s2);
} else { } else {
peg$currPos = s0; peg$currPos = s0;
s0 = peg$FAILED; s0 = peg$FAILED;
@@ -1165,7 +1227,7 @@ function peg$parse(input, options) {
peg$currPos++; peg$currPos++;
} else { } else {
s1 = peg$FAILED; s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e27); } if (peg$silentFails === 0) { peg$fail(peg$e29); }
} }
if (s1 !== peg$FAILED) { if (s1 !== peg$FAILED) {
s2 = peg$parsews(); s2 = peg$parsews();
@@ -1193,11 +1255,11 @@ function peg$parse(input, options) {
peg$currPos++; peg$currPos++;
} else { } else {
s13 = peg$FAILED; s13 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e28); } if (peg$silentFails === 0) { peg$fail(peg$e30); }
} }
if (s13 !== peg$FAILED) { if (s13 !== peg$FAILED) {
peg$savedPos = s0; peg$savedPos = s0;
s0 = peg$f8(s3, s7, s11); s0 = peg$f9(s3, s7, s11);
} else { } else {
peg$currPos = s0; peg$currPos = s0;
s0 = peg$FAILED; s0 = peg$FAILED;
@@ -1231,13 +1293,13 @@ function peg$parse(input, options) {
peg$currPos++; peg$currPos++;
} else { } else {
s1 = peg$FAILED; s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e29); } if (peg$silentFails === 0) { peg$fail(peg$e31); }
} }
if (s1 !== peg$FAILED) { if (s1 !== peg$FAILED) {
s2 = peg$parseslice(); s2 = peg$parseslice();
if (s2 !== peg$FAILED) { if (s2 !== peg$FAILED) {
peg$savedPos = s0; peg$savedPos = s0;
s0 = peg$f9(s2); s0 = peg$f10(s2);
} else { } else {
peg$currPos = s0; peg$currPos = s0;
s0 = peg$FAILED; s0 = peg$FAILED;
@@ -1259,13 +1321,13 @@ function peg$parse(input, options) {
peg$currPos++; peg$currPos++;
} else { } else {
s1 = peg$FAILED; s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e30); } if (peg$silentFails === 0) { peg$fail(peg$e32); }
} }
if (s1 !== peg$FAILED) { if (s1 !== peg$FAILED) {
s2 = peg$parseslice(); s2 = peg$parseslice();
if (s2 !== peg$FAILED) { if (s2 !== peg$FAILED) {
peg$savedPos = s0; peg$savedPos = s0;
s0 = peg$f10(s2); s0 = peg$f11(s2);
} else { } else {
peg$currPos = s0; peg$currPos = s0;
s0 = peg$FAILED; s0 = peg$FAILED;
@@ -1287,7 +1349,7 @@ function peg$parse(input, options) {
peg$currPos++; peg$currPos++;
} else { } else {
s1 = peg$FAILED; s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e31); } if (peg$silentFails === 0) { peg$fail(peg$e33); }
} }
if (s1 !== peg$FAILED) { if (s1 !== peg$FAILED) {
s2 = peg$parsenumber(); s2 = peg$parsenumber();
@@ -1295,7 +1357,7 @@ function peg$parse(input, options) {
s2 = null; s2 = null;
} }
peg$savedPos = s0; peg$savedPos = s0;
s0 = peg$f11(s2); s0 = peg$f12(s2);
} else { } else {
peg$currPos = s0; peg$currPos = s0;
s0 = peg$FAILED; s0 = peg$FAILED;
@@ -1313,13 +1375,13 @@ function peg$parse(input, options) {
peg$currPos++; peg$currPos++;
} else { } else {
s1 = peg$FAILED; s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e32); } if (peg$silentFails === 0) { peg$fail(peg$e34); }
} }
if (s1 !== peg$FAILED) { if (s1 !== peg$FAILED) {
s2 = peg$parseslice(); s2 = peg$parseslice();
if (s2 !== peg$FAILED) { if (s2 !== peg$FAILED) {
peg$savedPos = s0; peg$savedPos = s0;
s0 = peg$f12(s2); s0 = peg$f13(s2);
} else { } else {
peg$currPos = s0; peg$currPos = s0;
s0 = peg$FAILED; s0 = peg$FAILED;
@@ -1345,7 +1407,7 @@ function peg$parse(input, options) {
s3 = peg$parseslice_op(); s3 = peg$parseslice_op();
} }
peg$savedPos = s0; peg$savedPos = s0;
s0 = peg$f13(s1, s2); s0 = peg$f14(s1, s2);
} else { } else {
peg$currPos = s0; peg$currPos = s0;
s0 = peg$FAILED; s0 = peg$FAILED;
@@ -1370,7 +1432,7 @@ function peg$parse(input, options) {
} }
if (s1 !== peg$FAILED) { if (s1 !== peg$FAILED) {
peg$savedPos = s0; peg$savedPos = s0;
s1 = peg$f14(s1); s1 = peg$f15(s1);
} }
s0 = s1; s0 = s1;
@@ -1419,7 +1481,7 @@ function peg$parse(input, options) {
} }
if (s1 !== peg$FAILED) { if (s1 !== peg$FAILED) {
peg$savedPos = s0; peg$savedPos = s0;
s1 = peg$f15(s1); s1 = peg$f16(s1);
} }
s0 = s1; s0 = s1;
@@ -1468,7 +1530,7 @@ function peg$parse(input, options) {
} }
if (s1 !== peg$FAILED) { if (s1 !== peg$FAILED) {
peg$savedPos = s0; peg$savedPos = s0;
s1 = peg$f16(s1); s1 = peg$f17(s1);
} }
s0 = s1; s0 = s1;
@@ -1489,7 +1551,7 @@ function peg$parse(input, options) {
s2 = null; s2 = null;
} }
peg$savedPos = s0; peg$savedPos = s0;
s0 = peg$f17(s1, s2); s0 = peg$f18(s1, s2);
} else { } else {
peg$currPos = s0; peg$currPos = s0;
s0 = peg$FAILED; s0 = peg$FAILED;
@@ -1509,7 +1571,7 @@ function peg$parse(input, options) {
s2 = null; s2 = null;
} }
peg$savedPos = s0; peg$savedPos = s0;
s0 = peg$f18(s1, s2); s0 = peg$f19(s1, s2);
} else { } else {
peg$currPos = s0; peg$currPos = s0;
s0 = peg$FAILED; s0 = peg$FAILED;
@@ -1519,20 +1581,18 @@ function peg$parse(input, options) {
} }
function peg$parsemini() { function peg$parsemini() {
var s0, s1, s2, s3, s4, s5, s6; var s0, s1, s2, s3, s4;
s0 = peg$currPos; s0 = peg$currPos;
s1 = peg$parsews(); s1 = peg$parsews();
s2 = peg$parsequote(); s2 = peg$parsequote();
if (s2 !== peg$FAILED) { if (s2 !== peg$FAILED) {
s3 = peg$parsews(); s3 = peg$parsestack_or_choose();
s4 = peg$parsestack_or_choose(); if (s3 !== peg$FAILED) {
s4 = peg$parsequote();
if (s4 !== peg$FAILED) { if (s4 !== peg$FAILED) {
s5 = peg$parsews();
s6 = peg$parsequote();
if (s6 !== peg$FAILED) {
peg$savedPos = s0; peg$savedPos = s0;
s0 = peg$f19(s4); s0 = peg$f20(s3);
} else { } else {
peg$currPos = s0; peg$currPos = s0;
s0 = peg$FAILED; s0 = peg$FAILED;
@@ -1587,14 +1647,14 @@ function peg$parse(input, options) {
peg$currPos += 6; peg$currPos += 6;
} else { } else {
s1 = peg$FAILED; s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e33); } if (peg$silentFails === 0) { peg$fail(peg$e35); }
} }
if (s1 !== peg$FAILED) { if (s1 !== peg$FAILED) {
s2 = peg$parsews(); s2 = peg$parsews();
s3 = peg$parsemini_or_operator(); s3 = peg$parsemini_or_operator();
if (s3 !== peg$FAILED) { if (s3 !== peg$FAILED) {
peg$savedPos = s0; peg$savedPos = s0;
s0 = peg$f20(s3); s0 = peg$f21(s3);
} else { } else {
peg$currPos = s0; peg$currPos = s0;
s0 = peg$FAILED; s0 = peg$FAILED;
@@ -1616,7 +1676,7 @@ function peg$parse(input, options) {
peg$currPos += 6; peg$currPos += 6;
} else { } else {
s1 = peg$FAILED; s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e34); } if (peg$silentFails === 0) { peg$fail(peg$e36); }
} }
if (s1 !== peg$FAILED) { if (s1 !== peg$FAILED) {
s2 = peg$parsews(); s2 = peg$parsews();
@@ -1627,7 +1687,7 @@ function peg$parse(input, options) {
s5 = peg$parsequote(); s5 = peg$parsequote();
if (s5 !== peg$FAILED) { if (s5 !== peg$FAILED) {
peg$savedPos = s0; peg$savedPos = s0;
s0 = peg$f21(s4); s0 = peg$f22(s4);
} else { } else {
peg$currPos = s0; peg$currPos = s0;
s0 = peg$FAILED; s0 = peg$FAILED;
@@ -1657,7 +1717,7 @@ function peg$parse(input, options) {
peg$currPos += 6; peg$currPos += 6;
} else { } else {
s1 = peg$FAILED; s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e35); } if (peg$silentFails === 0) { peg$fail(peg$e37); }
} }
if (s1 !== peg$FAILED) { if (s1 !== peg$FAILED) {
s2 = peg$parsews(); s2 = peg$parsews();
@@ -1672,7 +1732,7 @@ function peg$parse(input, options) {
s7 = null; s7 = null;
} }
peg$savedPos = s0; peg$savedPos = s0;
s0 = peg$f22(s3, s5, s7); s0 = peg$f23(s3, s5, s7);
} else { } else {
peg$currPos = s0; peg$currPos = s0;
s0 = peg$FAILED; s0 = peg$FAILED;
@@ -1698,14 +1758,14 @@ function peg$parse(input, options) {
peg$currPos += 4; peg$currPos += 4;
} else { } else {
s1 = peg$FAILED; s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e36); } if (peg$silentFails === 0) { peg$fail(peg$e38); }
} }
if (s1 !== peg$FAILED) { if (s1 !== peg$FAILED) {
s2 = peg$parsews(); s2 = peg$parsews();
s3 = peg$parsenumber(); s3 = peg$parsenumber();
if (s3 !== peg$FAILED) { if (s3 !== peg$FAILED) {
peg$savedPos = s0; peg$savedPos = s0;
s0 = peg$f23(s3); s0 = peg$f24(s3);
} else { } else {
peg$currPos = s0; peg$currPos = s0;
s0 = peg$FAILED; s0 = peg$FAILED;
@@ -1727,14 +1787,14 @@ function peg$parse(input, options) {
peg$currPos += 4; peg$currPos += 4;
} else { } else {
s1 = peg$FAILED; s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e37); } if (peg$silentFails === 0) { peg$fail(peg$e39); }
} }
if (s1 !== peg$FAILED) { if (s1 !== peg$FAILED) {
s2 = peg$parsews(); s2 = peg$parsews();
s3 = peg$parsenumber(); s3 = peg$parsenumber();
if (s3 !== peg$FAILED) { if (s3 !== peg$FAILED) {
peg$savedPos = s0; peg$savedPos = s0;
s0 = peg$f24(s3); s0 = peg$f25(s3);
} else { } else {
peg$currPos = s0; peg$currPos = s0;
s0 = peg$FAILED; s0 = peg$FAILED;
@@ -1756,14 +1816,14 @@ function peg$parse(input, options) {
peg$currPos += 4; peg$currPos += 4;
} else { } else {
s1 = peg$FAILED; s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e38); } if (peg$silentFails === 0) { peg$fail(peg$e40); }
} }
if (s1 !== peg$FAILED) { if (s1 !== peg$FAILED) {
s2 = peg$parsews(); s2 = peg$parsews();
s3 = peg$parsenumber(); s3 = peg$parsenumber();
if (s3 !== peg$FAILED) { if (s3 !== peg$FAILED) {
peg$savedPos = s0; peg$savedPos = s0;
s0 = peg$f25(s3); s0 = peg$f26(s3);
} else { } else {
peg$currPos = s0; peg$currPos = s0;
s0 = peg$FAILED; s0 = peg$FAILED;
@@ -1785,14 +1845,14 @@ function peg$parse(input, options) {
peg$currPos += 4; peg$currPos += 4;
} else { } else {
s1 = peg$FAILED; s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e39); } if (peg$silentFails === 0) { peg$fail(peg$e41); }
} }
if (s1 !== peg$FAILED) { if (s1 !== peg$FAILED) {
s2 = peg$parsews(); s2 = peg$parsews();
s3 = peg$parsenumber(); s3 = peg$parsenumber();
if (s3 !== peg$FAILED) { if (s3 !== peg$FAILED) {
peg$savedPos = s0; peg$savedPos = s0;
s0 = peg$f26(s3); s0 = peg$f27(s3);
} else { } else {
peg$currPos = s0; peg$currPos = s0;
s0 = peg$FAILED; s0 = peg$FAILED;
@@ -1814,7 +1874,7 @@ function peg$parse(input, options) {
peg$currPos += 5; peg$currPos += 5;
} else { } else {
s1 = peg$FAILED; s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e40); } if (peg$silentFails === 0) { peg$fail(peg$e42); }
} }
if (s1 !== peg$FAILED) { if (s1 !== peg$FAILED) {
s2 = peg$parsews(); s2 = peg$parsews();
@@ -1834,7 +1894,7 @@ function peg$parse(input, options) {
s5 = peg$parsequote(); s5 = peg$parsequote();
if (s5 !== peg$FAILED) { if (s5 !== peg$FAILED) {
peg$savedPos = s0; peg$savedPos = s0;
s0 = peg$f27(s4); s0 = peg$f28(s4);
} else { } else {
peg$currPos = s0; peg$currPos = s0;
s0 = peg$FAILED; s0 = peg$FAILED;
@@ -1864,25 +1924,25 @@ function peg$parse(input, options) {
peg$currPos += 2; peg$currPos += 2;
} else { } else {
s1 = peg$FAILED; s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e41); } if (peg$silentFails === 0) { peg$fail(peg$e43); }
} }
if (s1 !== peg$FAILED) { if (s1 !== peg$FAILED) {
s2 = []; s2 = [];
if (peg$r5.test(input.charAt(peg$currPos))) { if (peg$r7.test(input.charAt(peg$currPos))) {
s3 = input.charAt(peg$currPos); s3 = input.charAt(peg$currPos);
peg$currPos++; peg$currPos++;
} else { } else {
s3 = peg$FAILED; s3 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e42); } if (peg$silentFails === 0) { peg$fail(peg$e44); }
} }
while (s3 !== peg$FAILED) { while (s3 !== peg$FAILED) {
s2.push(s3); s2.push(s3);
if (peg$r5.test(input.charAt(peg$currPos))) { if (peg$r7.test(input.charAt(peg$currPos))) {
s3 = input.charAt(peg$currPos); s3 = input.charAt(peg$currPos);
peg$currPos++; peg$currPos++;
} else { } else {
s3 = peg$FAILED; s3 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e42); } if (peg$silentFails === 0) { peg$fail(peg$e44); }
} }
} }
s1 = [s1, s2]; s1 = [s1, s2];
@@ -1904,7 +1964,7 @@ function peg$parse(input, options) {
peg$currPos += 3; peg$currPos += 3;
} else { } else {
s1 = peg$FAILED; s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e43); } if (peg$silentFails === 0) { peg$fail(peg$e45); }
} }
if (s1 !== peg$FAILED) { if (s1 !== peg$FAILED) {
s2 = peg$parsews(); s2 = peg$parsews();
@@ -1913,7 +1973,7 @@ function peg$parse(input, options) {
peg$currPos++; peg$currPos++;
} else { } else {
s3 = peg$FAILED; s3 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e18); } if (peg$silentFails === 0) { peg$fail(peg$e20); }
} }
if (s3 !== peg$FAILED) { if (s3 !== peg$FAILED) {
s4 = peg$parsews(); s4 = peg$parsews();
@@ -1926,7 +1986,7 @@ function peg$parse(input, options) {
s9 = peg$parsemini_or_operator(); s9 = peg$parsemini_or_operator();
if (s9 !== peg$FAILED) { if (s9 !== peg$FAILED) {
peg$savedPos = s7; peg$savedPos = s7;
s7 = peg$f28(s5, s9); s7 = peg$f29(s5, s9);
} else { } else {
peg$currPos = s7; peg$currPos = s7;
s7 = peg$FAILED; s7 = peg$FAILED;
@@ -1943,7 +2003,7 @@ function peg$parse(input, options) {
s9 = peg$parsemini_or_operator(); s9 = peg$parsemini_or_operator();
if (s9 !== peg$FAILED) { if (s9 !== peg$FAILED) {
peg$savedPos = s7; peg$savedPos = s7;
s7 = peg$f28(s5, s9); s7 = peg$f29(s5, s9);
} else { } else {
peg$currPos = s7; peg$currPos = s7;
s7 = peg$FAILED; s7 = peg$FAILED;
@@ -1959,11 +2019,11 @@ function peg$parse(input, options) {
peg$currPos++; peg$currPos++;
} else { } else {
s8 = peg$FAILED; s8 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e19); } if (peg$silentFails === 0) { peg$fail(peg$e21); }
} }
if (s8 !== peg$FAILED) { if (s8 !== peg$FAILED) {
peg$savedPos = s0; peg$savedPos = s0;
s0 = peg$f29(s5, s6); s0 = peg$f30(s5, s6);
} else { } else {
peg$currPos = s0; peg$currPos = s0;
s0 = peg$FAILED; s0 = peg$FAILED;
@@ -2009,7 +2069,7 @@ function peg$parse(input, options) {
s4 = peg$parsecomment(); s4 = peg$parsecomment();
} }
peg$savedPos = s0; peg$savedPos = s0;
s0 = peg$f30(s1); s0 = peg$f31(s1);
} else { } else {
peg$currPos = s0; peg$currPos = s0;
s0 = peg$FAILED; s0 = peg$FAILED;
@@ -2024,14 +2084,14 @@ function peg$parse(input, options) {
peg$currPos++; peg$currPos++;
} else { } else {
s3 = peg$FAILED; s3 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e44); } if (peg$silentFails === 0) { peg$fail(peg$e46); }
} }
if (s3 !== peg$FAILED) { if (s3 !== peg$FAILED) {
s4 = peg$parsews(); s4 = peg$parsews();
s5 = peg$parsemini_or_operator(); s5 = peg$parsemini_or_operator();
if (s5 !== peg$FAILED) { if (s5 !== peg$FAILED) {
peg$savedPos = s0; peg$savedPos = s0;
s0 = peg$f31(s1, s5); s0 = peg$f32(s1, s5);
} else { } else {
peg$currPos = s0; peg$currPos = s0;
s0 = peg$FAILED; s0 = peg$FAILED;
@@ -2056,7 +2116,7 @@ function peg$parse(input, options) {
s1 = peg$parsemini_or_operator(); s1 = peg$parsemini_or_operator();
if (s1 !== peg$FAILED) { if (s1 !== peg$FAILED) {
peg$savedPos = s0; peg$savedPos = s0;
s1 = peg$f32(s1); s1 = peg$f33(s1);
} }
s0 = s1; s0 = s1;
if (s0 === peg$FAILED) { if (s0 === peg$FAILED) {
@@ -2089,7 +2149,7 @@ function peg$parse(input, options) {
if (s2 !== peg$FAILED) { if (s2 !== peg$FAILED) {
s3 = peg$parsews(); s3 = peg$parsews();
peg$savedPos = s0; peg$savedPos = s0;
s0 = peg$f33(s2); s0 = peg$f34(s2);
} else { } else {
peg$currPos = s0; peg$currPos = s0;
s0 = peg$FAILED; s0 = peg$FAILED;
@@ -2107,14 +2167,14 @@ function peg$parse(input, options) {
peg$currPos += 6; peg$currPos += 6;
} else { } else {
s1 = peg$FAILED; s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e45); } if (peg$silentFails === 0) { peg$fail(peg$e47); }
} }
if (s1 !== peg$FAILED) { if (s1 !== peg$FAILED) {
s2 = peg$parsews(); s2 = peg$parsews();
s3 = peg$parsenumber(); s3 = peg$parsenumber();
if (s3 !== peg$FAILED) { if (s3 !== peg$FAILED) {
peg$savedPos = s0; peg$savedPos = s0;
s0 = peg$f34(s3); s0 = peg$f35(s3);
} else { } else {
peg$currPos = s0; peg$currPos = s0;
s0 = peg$FAILED; s0 = peg$FAILED;
@@ -2136,14 +2196,14 @@ function peg$parse(input, options) {
peg$currPos += 6; peg$currPos += 6;
} else { } else {
s1 = peg$FAILED; s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e46); } if (peg$silentFails === 0) { peg$fail(peg$e48); }
} }
if (s1 !== peg$FAILED) { if (s1 !== peg$FAILED) {
s2 = peg$parsews(); s2 = peg$parsews();
s3 = peg$parsenumber(); s3 = peg$parsenumber();
if (s3 !== peg$FAILED) { if (s3 !== peg$FAILED) {
peg$savedPos = s0; peg$savedPos = s0;
s0 = peg$f35(s3); s0 = peg$f36(s3);
} else { } else {
peg$currPos = s0; peg$currPos = s0;
s0 = peg$FAILED; s0 = peg$FAILED;
@@ -2165,11 +2225,11 @@ function peg$parse(input, options) {
peg$currPos += 4; peg$currPos += 4;
} else { } else {
s1 = peg$FAILED; s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e47); } if (peg$silentFails === 0) { peg$fail(peg$e49); }
} }
if (s1 !== peg$FAILED) { if (s1 !== peg$FAILED) {
peg$savedPos = s0; peg$savedPos = s0;
s1 = peg$f36(); s1 = peg$f37();
} }
s0 = s1; s0 = s1;
@@ -2195,13 +2255,17 @@ function peg$parse(input, options) {
this.location_ = location(); this.location_ = location();
} }
var PatternStub = function(source, alignment, seed) var MemoryStub = function(source)
{
this.type_ = "memory";
this.source_ = source;
this.location_ = location();
}
var PatternStub = function(source, alignment)
{ {
this.type_ = "pattern"; this.type_ = "pattern";
this.arguments_ = { alignment: alignment }; this.arguments_ = { alignment : alignment};
if (seed !== undefined) {
this.arguments_.seed = seed;
}
this.source_ = source; this.source_ = source;
} }
@@ -2227,7 +2291,6 @@ function peg$parse(input, options) {
this.options_ = options; this.options_ = options;
} }
var seed = 0;
peg$result = peg$startRuleFunction(); peg$result = peg$startRuleFunction();
+17 -12
View File
@@ -19,13 +19,17 @@ This program is free software: you can redistribute it and/or modify it under th
this.location_ = location(); this.location_ = location();
} }
var PatternStub = function(source, alignment, seed) var MemoryStub = function(source)
{
this.type_ = "memory";
this.source_ = source;
this.location_ = location();
}
var PatternStub = function(source, alignment)
{ {
this.type_ = "pattern"; this.type_ = "pattern";
this.arguments_ = { alignment: alignment }; this.arguments_ = { alignment : alignment};
if (seed !== undefined) {
this.arguments_.seed = seed;
}
this.source_ = source; this.source_ = source;
} }
@@ -51,7 +55,6 @@ This program is free software: you can redistribute it and/or modify it under th
this.options_ = options; this.options_ = options;
} }
var seed = 0;
} }
start = statement start = statement
@@ -100,9 +103,11 @@ quote = '"' / "'"
// ------------------ steps and cycles --------------------------- // ------------------ steps and cycles ---------------------------
// single step definition (e.g bd) // single step definition (e.g bd)
step_char = [0-9a-zA-Z~] / "-" / "#" / "." / "^" / "_" step_char = [0-9a-zA-Z~] / "-" / "#" / "." / "_" / "^"
step = ws chars:step_char+ ws { return new AtomStub(chars.join("")) } step = ws chars:step_char+ ws { return new AtomStub(chars.join("")) }
memory = ws "^" h:[A-Za-z] t:[A-Za-z0-9_]+ { return new MemoryStub(h + t.join("")) }
// define a sub cycle e.g. [1 2, 3 [4]] // define a sub cycle e.g. [1 2, 3 [4]]
sub_cycle = ws "[" ws s:stack_or_choose ws "]" ws { return s } sub_cycle = ws "[" ws s:stack_or_choose ws "]" ws { return s }
@@ -119,7 +124,7 @@ slow_sequence = ws "<" ws s:sequence ws ">" ws
{ s.arguments_.alignment = 'slowcat'; return s; } { s.arguments_.alignment = 'slowcat'; return s; }
// a slice is either a single step or a sub cycle // a slice is either a single step or a sub cycle
slice = step / sub_cycle / polymeter / slow_sequence slice = memory / step / sub_cycle / polymeter / slow_sequence
// slice modifier affects the timing/size of a slice (e.g. [a b c]@3) // 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 // at this point, we assume we can represent them as regular sequence operators
@@ -141,7 +146,7 @@ op_fast = "*"a:slice
{ return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'fast' }}) } { return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'fast' }}) }
op_degrade = "?"a:number? op_degrade = "?"a:number?
{ return x => x.options_['ops'].push({ type_: "degradeBy", arguments_ :{ amount:a, seed: seed++ } }) } { return x => x.options_['ops'].push({ type_: "degradeBy", arguments_ :{ amount:a } }) }
op_tail = ":" s:slice op_tail = ":" s:slice
{ return x => x.options_['ops'].push({ type_: "tail", arguments_ :{ element:s } }) } { return x => x.options_['ops'].push({ type_: "tail", arguments_ :{ element:s } }) }
@@ -166,12 +171,12 @@ stack_tail = tail:(comma @sequence)+
// a choose is a series of pipe-separated sequence, one of which is // a choose is a series of pipe-separated sequence, one of which is
// chosen at random, each cycle // chosen at random, each cycle
choose_tail = tail:(pipe @sequence)+ choose_tail = tail:(pipe @sequence)+
{ return { alignment: 'rand', list: tail, seed: seed++ }; } { return { alignment: 'rand', list: tail }; }
// if the stack contains only one element, we don't create a stack but return the // if the stack contains only one element, we don't create a stack but return the
// underlying element // underlying element
stack_or_choose = head:sequence tail:(stack_tail / choose_tail)? stack_or_choose = head:sequence tail:(stack_tail / choose_tail)?
{ if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment, tail.seed); } else { return head; } } { if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment); } else { return head; } }
polymeter_stack = head:sequence tail:stack_tail? polymeter_stack = head:sequence tail:stack_tail?
{ return new PatternStub(tail ? [head, ...tail.list] : [head], 'polymeter'); } { return new PatternStub(tail ? [head, ...tail.list] : [head], 'polymeter'); }
@@ -182,7 +187,7 @@ polymeter_stack = head:sequence tail:stack_tail?
// Experimental haskellish parser begins // Experimental haskellish parser begins
// mini-notation = a quoted stack // mini-notation = a quoted stack
mini = ws quote ws sc:stack_or_choose ws quote mini = ws quote sc:stack_or_choose quote
{ return sc; } { return sc; }
// ------------------ operators --------------------------- // ------------------ operators ---------------------------
+51 -10
View File
@@ -7,7 +7,23 @@ This program is free software: you can redistribute it and/or modify it under th
import * as krill from './krill-parser.js'; import * as krill from './krill-parser.js';
import * as strudel from '@strudel.cycles/core'; import * as strudel from '@strudel.cycles/core';
const randOffset = 0.0003; /* var _seedState = 0;
const randOffset = 0.0002;
function _nextSeed() {
return _seedState++;
} */
const memory = {};
export const remember = (name, pat) => {
if (pat) {
memory[name] = pat;
} else {
pat = memory[name];
}
return pat;
};
const applyOptions = (parent, code) => (pat, i) => { const applyOptions = (parent, code) => (pat, i) => {
const ast = parent.source_[i]; const ast = parent.source_[i];
@@ -39,9 +55,27 @@ const applyOptions = (parent, code) => (pat, i) => {
break; break;
} }
case 'degradeBy': { case 'degradeBy': {
pat = strudel // TODO: find out what is right here
.reify(pat) // example:
._degradeByWith(strudel.rand.early(randOffset * op.arguments_.seed), op.arguments_.amount ?? 0.5); /*
stack(
s("hh*8").degrade(),
s("[ht*8]?")
)
*/
// above example will only be in sync when _degradeBy is used...
// it also seems that the nextSeed will create undeterministic behaviour
// as it uses a global _seedState. This is probably the reason for
// https://github.com/tidalcycles/strudel/issues/245
// this is how it was:
/*
return strudel.reify(pat)._degradeByWith(
strudel.rand.early(randOffset * _nextSeed()).segment(1),
op.arguments_.amount ?? 0.5,
);
*/
pat = strudel.reify(pat).degradeBy(op.arguments_.amount === null ? 0.5 : op.arguments_.amount);
break; break;
} }
case 'tail': { case 'tail': {
@@ -91,7 +125,9 @@ export function patternifyAST(ast, code) {
return strudel.stack(...aligned); return strudel.stack(...aligned);
} }
if (alignment === 'rand') { if (alignment === 'rand') {
return strudel.chooseInWith(strudel.rand.early(randOffset * ast.arguments_.seed).segment(1), children); // https://github.com/tidalcycles/strudel/issues/245#issuecomment-1345406422
// return strudel.chooseInWith(strudel.rand.early(randOffset * _nextSeed()).segment(1), children);
return strudel.chooseCycles(...children);
} }
const weightedChildren = ast.source_.some((child) => !!child.options_?.weight); const weightedChildren = ast.source_.some((child) => !!child.options_?.weight);
if (!weightedChildren && alignment === 'slowcat') { if (!weightedChildren && alignment === 'slowcat') {
@@ -113,6 +149,16 @@ export function patternifyAST(ast, code) {
case 'element': { case 'element': {
return patternifyAST(ast.source_, code); return patternifyAST(ast.source_, code);
} }
case 'memory': {
const name = ast.source_;
if (name in memory) {
return new strudel.Pattern((state) => {
return memory[name].query(state);
});
}
// TODO - how to default a memory.. via a list?
return strudel.pure(undefined);
}
case 'atom': { case 'atom': {
if (ast.source_ === '~') { if (ast.source_ === '~') {
return strudel.silence; return strudel.silence;
@@ -190,8 +236,3 @@ export function minify(thing) {
} }
return strudel.reify(thing); return strudel.reify(thing);
} }
// calling this function will cause patterns to parse strings as mini notation by default
export function miniAllStrings() {
strudel.setStringParser(mini);
}
+4 -4
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel.cycles/mini", "name": "@strudel.cycles/mini",
"version": "0.8.2", "version": "0.6.0",
"description": "Mini notation for strudel", "description": "Mini notation for strudel",
"main": "index.mjs", "main": "index.mjs",
"type": "module", "type": "module",
@@ -35,8 +35,8 @@
"@strudel.cycles/core": "workspace:*" "@strudel.cycles/core": "workspace:*"
}, },
"devDependencies": { "devDependencies": {
"peggy": "^3.0.2", "peggy": "^2.0.1",
"vite": "^4.3.3", "vite": "^3.2.2",
"vitest": "^0.28.0" "vitest": "^0.25.7"
} }
} }
+5 -19
View File
@@ -140,22 +140,10 @@ describe('mini', () => {
expect(haps.length < 230).toBe(true); expect(haps.length < 230).toBe(true);
// 'Had too many cycles remaining after degradeBy 0.8'); // 'Had too many cycles remaining after degradeBy 0.8');
}); });
it('supports multiple independent uses of the random choice operator ("|")', () => { it('supports lists', () => {
const numCycles = 1000; expect(minV('a:b c:d:[e:f] g')).toEqual([['a', 'b'], ['c', 'd', ['e', 'f']], 'g']);
const values = mini('[a|b] [a|b]')
.queryArc(0, numCycles)
.map((e) => e.value);
const observed = { aa: 0, ab: 0, ba: 0, bb: 0 };
for (let i = 0; i < values.length; i += 2) {
const chunk = values.slice(i, i + 2);
observed[chunk.join('')]++;
}
for (const count of Object.values(observed)) {
// Should fall within 99% confidence interval for binomial with p=0.25.
expect(215 <= count && count <= 286).toBe(true);
}
}); });
it('supports the random choice operator ("|") with nesting', () => { /*it('supports the random choice operator ("|") with nesting', () => {
const numCycles = 900; const numCycles = 900;
const haps = mini('a | [b | c] | [d | e | f]').queryArc(0, numCycles); const haps = mini('a | [b | c] | [d | e | f]').queryArc(0, numCycles);
// Should have about 1/3 a, 1/6 each of b | c, and 1/9 each of d | e | f. // Should have about 1/3 a, 1/6 each of b | c, and 1/9 each of d | e | f.
@@ -180,8 +168,6 @@ describe('mini', () => {
// 15.086 is the chisq for 5 degrees of freedom at 99%, so for 99% of uniformly-distributed // 15.086 is the chisq for 5 degrees of freedom at 99%, so for 99% of uniformly-distributed
// PRNG, this test should succeed // PRNG, this test should succeed
expect(chisq <= 15.086).toBe(true); expect(chisq <= 15.086).toBe(true);
}); // assert(chisq <= 15.086, chisq + ' was expected to be less than 15.086 under chi-squared test');
it('supports lists', () => { });*/
expect(minV('a:b c:d:[e:f] g')).toEqual([['a', 'b'], ['c', 'd', ['e', 'f']], 'g']);
});
}); });
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel.cycles/osc", "name": "@strudel.cycles/osc",
"version": "0.8.0", "version": "0.6.0",
"description": "OSC messaging for strudel", "description": "OSC messaging for strudel",
"main": "osc.mjs", "main": "osc.mjs",
"publishConfig": { "publishConfig": {
@@ -40,7 +40,7 @@
"osc-js": "^2.4.0" "osc-js": "^2.4.0"
}, },
"devDependencies": { "devDependencies": {
"pkg": "^5.8.1", "pkg": "^5.7.0",
"vite": "^4.3.3" "vite": "^3.2.2"
} }
} }
@@ -15,18 +15,17 @@
"@strudel.cycles/osc": "workspace:*", "@strudel.cycles/osc": "workspace:*",
"@strudel.cycles/mini": "workspace:*", "@strudel.cycles/mini": "workspace:*",
"@strudel.cycles/transpiler": "workspace:*", "@strudel.cycles/transpiler": "workspace:*",
"@strudel.cycles/soundfonts": "workspace:*",
"@strudel.cycles/webaudio": "workspace:*", "@strudel.cycles/webaudio": "workspace:*",
"@strudel.cycles/tonal": "workspace:*", "@strudel.cycles/tonal": "workspace:*",
"@strudel.cycles/react": "workspace:*" "@strudel.cycles/react": "workspace:*"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^18.2.0", "@types/react": "^18.0.17",
"@types/react-dom": "^18.2.1", "@types/react-dom": "^18.0.6",
"@vitejs/plugin-react": "^4.0.0", "@vitejs/plugin-react": "^2.0.1",
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.8",
"postcss": "^8.4.23", "postcss": "^8.4.16",
"tailwindcss": "^3.3.2", "tailwindcss": "^3.1.8",
"vite": "^4.3.3" "vite": "^3.0.7"
} }
} }
+8 -20
View File
@@ -1,22 +1,14 @@
import { controls, evalScope } from '@strudel.cycles/core'; import { controls, evalScope } from '@strudel.cycles/core';
import { CodeMirror, useHighlighting, useKeydown, useStrudel, flash } from '@strudel.cycles/react'; import { CodeMirror, useHighlighting, useKeydown, useStrudel, flash } from '@strudel.cycles/react';
import { import { getAudioContext, initAudioOnFirstClick, panic, webaudioOutput } from '@strudel.cycles/webaudio';
getAudioContext,
initAudioOnFirstClick,
panic,
webaudioOutput,
registerSynthSounds,
} from '@strudel.cycles/webaudio';
import { registerSoundfonts } from '@strudel.cycles/soundfonts';
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import './style.css'; import './style.css';
// import { prebake } from '../../../../../repl/src/prebake.mjs'; // import { prebake } from '../../../../../repl/src/prebake.mjs';
initAudioOnFirstClick(); initAudioOnFirstClick();
async function init() { // TODO: only import stuff when play is pressed?
// TODO: only import stuff when play is pressed? evalScope(
const loadModules = evalScope(
controls, controls,
import('@strudel.cycles/core'), import('@strudel.cycles/core'),
import('@strudel.cycles/tonal'), import('@strudel.cycles/tonal'),
@@ -24,11 +16,7 @@ async function init() {
import('@strudel.cycles/xen'), import('@strudel.cycles/xen'),
import('@strudel.cycles/webaudio'), import('@strudel.cycles/webaudio'),
import('@strudel.cycles/osc'), import('@strudel.cycles/osc'),
); );
await Promise.all([loadModules, registerSynthSounds(), registerSoundfonts()]);
}
init();
const defaultTune = `samples({ const defaultTune = `samples({
bd: ['bd/BT0AADA.wav','bd/BT0AAD0.wav','bd/BT0A0DA.wav','bd/BT0A0D3.wav','bd/BT0A0D0.wav','bd/BT0A0A7.wav'], bd: ['bd/BT0AADA.wav','bd/BT0AAD0.wav','bd/BT0A0DA.wav','bd/BT0A0D3.wav','bd/BT0A0D0.wav','bd/BT0A0A7.wav'],
@@ -43,7 +31,7 @@ stack(
.off(1/8,x=>x.add(12).degradeBy(.5)) // random octave jumps .off(1/8,x=>x.add(12).degradeBy(.5)) // random octave jumps
.add(perlin.range(0,.5)) // random pitch variation .add(perlin.range(0,.5)) // random pitch variation
.superimpose(add(.05)) // add second, slightly detuned voice .superimpose(add(.05)) // add second, slightly detuned voice
.note() // wrap in "note" .n() // wrap in "n"
.decay(.15).sustain(0) // make each note of equal length .decay(.15).sustain(0) // make each note of equal length
.s('sawtooth') // waveform .s('sawtooth') // waveform
.gain(.4) // turn down .gain(.4) // turn down
@@ -52,7 +40,7 @@ stack(
,"<Am7!3 <Em7 E7b13 Em7 Ebm7b5>>".voicings('lefthand') // chords ,"<Am7!3 <Em7 E7b13 Em7 Ebm7b5>>".voicings('lefthand') // chords
.superimpose(x=>x.add(.04)) // add second, slightly detuned voice .superimpose(x=>x.add(.04)) // add second, slightly detuned voice
.add(perlin.range(0,.5)) // random pitch variation .add(perlin.range(0,.5)) // random pitch variation
.note() // wrap in "n" .n() // wrap in "n"
.s('square') // waveform .s('square') // waveform
.gain(.16) // turn down .gain(.16) // turn down
.cutoff(500) // fixed cutoff .cutoff(500) // fixed cutoff
@@ -61,7 +49,7 @@ stack(
,"a4 c5 <e6 a6>".struct("x(5,8)") ,"a4 c5 <e6 a6>".struct("x(5,8)")
.superimpose(x=>x.add(.04)) // add second, slightly detuned voice .superimpose(x=>x.add(.04)) // add second, slightly detuned voice
.add(perlin.range(0,.5)) // random pitch variation .add(perlin.range(0,.5)) // random pitch variation
.note() // wrap in "note" .n() // wrap in "n"
.decay(.1).sustain(0) // make notes short .decay(.1).sustain(0) // make notes short
.s('triangle') // waveform .s('triangle') // waveform
.degradeBy(perlin.range(0,.5)) // randomly controlled random removal :) .degradeBy(perlin.range(0,.5)) // randomly controlled random removal :)
@@ -115,7 +103,7 @@ function App() {
} }
} }
}, },
[scheduler, evaluate, view, code], [scheduler, evaluate, view],
), ),
); );
return ( return (
+20 -20
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel.cycles/react", "name": "@strudel.cycles/react",
"version": "0.8.0", "version": "0.6.4",
"description": "React components for strudel", "description": "React components for strudel",
"main": "src/index.js", "main": "src/index.js",
"publishConfig": { "publishConfig": {
@@ -32,33 +32,33 @@
}, },
"homepage": "https://github.com/tidalcycles/strudel#readme", "homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "^6.6.0", "@codemirror/autocomplete": "^6.4.0",
"@codemirror/lang-javascript": "^6.1.7", "@codemirror/lang-javascript": "^6.1.1",
"@codemirror/state": "^6.2.0", "@codemirror/state": "^6.2.0",
"@codemirror/view": "^6.10.0", "@codemirror/view": "^6.7.3",
"@lezer/highlight": "^1.1.4", "@lezer/highlight": "^1.1.3",
"@replit/codemirror-emacs": "^6.0.1", "@replit/codemirror-emacs": "^6.0.0",
"@replit/codemirror-vim": "^6.0.14", "@replit/codemirror-vim": "^6.0.6",
"@strudel.cycles/core": "workspace:*", "@strudel.cycles/core": "workspace:*",
"@strudel.cycles/transpiler": "workspace:*", "@strudel.cycles/transpiler": "workspace:*",
"@strudel.cycles/webaudio": "workspace:*", "@strudel.cycles/webaudio": "workspace:*",
"@uiw/codemirror-themes": "^4.19.16", "@uiw/codemirror-themes": "^4.12.4",
"@uiw/react-codemirror": "^4.19.16", "@uiw/react-codemirror": "^4.12.4",
"react-hook-inview": "^4.5.0" "react-hook-inview": "^4.5.0"
}, },
"peerDependencies": { "peerDependencies": {
"react": "^18.2.0", "react": "^17.0.2",
"react-dom": "^18.2.0" "react-dom": "^17.0.2"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^18.2.0", "@types/react": "^17.0.2",
"@types/react-dom": "^18.2.1", "@types/react-dom": "^17.0.2",
"@vitejs/plugin-react": "^4.0.0", "@vitejs/plugin-react": "^2.2.0",
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.7",
"postcss": "^8.4.23", "postcss": "^8.4.18",
"react": "^18.2.0", "react": "^17.0.2",
"react-dom": "^18.2.0", "react-dom": "^17.0.2",
"tailwindcss": "^3.3.2", "tailwindcss": "^3.0.24",
"vite": "^4.3.3" "vite": "^3.2.2"
} }
} }
+2
View File
@@ -8,6 +8,8 @@ evalScope(
import('@strudel.cycles/core'), import('@strudel.cycles/core'),
import('@strudel.cycles/tonal'), import('@strudel.cycles/tonal'),
import('@strudel.cycles/mini'), import('@strudel.cycles/mini'),
import('@strudel.cycles/midi'),
import('@strudel.cycles/xen'),
import('@strudel.cycles/webaudio'), import('@strudel.cycles/webaudio'),
); );
+16 -34
View File
@@ -2,12 +2,12 @@ import React, { useMemo } from 'react';
import _CodeMirror from '@uiw/react-codemirror'; import _CodeMirror from '@uiw/react-codemirror';
import { EditorView, Decoration } from '@codemirror/view'; import { EditorView, Decoration } from '@codemirror/view';
import { StateField, StateEffect } from '@codemirror/state'; import { StateField, StateEffect } from '@codemirror/state';
import { javascript, javascriptLanguage } from '@codemirror/lang-javascript'; import { javascript } from '@codemirror/lang-javascript';
import strudelTheme from '../themes/strudel-theme'; import strudelTheme from '../themes/strudel-theme';
import './style.css'; import './style.css';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { autocompletion } from '@codemirror/autocomplete'; import { autocompletion } from '@codemirror/autocomplete';
import { strudelAutocomplete } from './Autocomplete'; //import { strudelAutocomplete } from './Autocomplete';
import { vim } from '@replit/codemirror-vim'; import { vim } from '@replit/codemirror-vim';
import { emacs } from '@replit/codemirror-emacs'; import { emacs } from '@replit/codemirror-emacs';
@@ -58,19 +58,15 @@ const highlightField = StateField.define({
haps haps
.map((hap) => .map((hap) =>
(hap.context.locations || []).map(({ start, end }) => { (hap.context.locations || []).map(({ start, end }) => {
const color = hap.context.color || e.value.color; // const color = hap.context.color || e.value.color || '#FFCA28';
let from = tr.newDoc.line(start.line).from + start.column; let from = tr.newDoc.line(start.line).from + start.column;
let to = tr.newDoc.line(end.line).from + end.column; let to = tr.newDoc.line(end.line).from + end.column;
const l = tr.newDoc.length; const l = tr.newDoc.length;
if (from > l || to > l) { if (from > l || to > l) {
return; // dont mark outside of range, as it will throw an error return; // dont mark outside of range, as it will throw an error
} }
let mark; //const mark = Decoration.mark({ attributes: { style: `outline: 2px solid ${color};` } });
if (color) { const mark = Decoration.mark({ attributes: { class: `outline outline-2 outline-foreground` } });
mark = Decoration.mark({ attributes: { style: `outline: 2px solid ${color};` } });
} else {
mark = Decoration.mark({ attributes: { class: `outline outline-2 outline-foreground` } });
}
return mark.range(from, to); return mark.range(from, to);
}), }),
) )
@@ -88,7 +84,14 @@ const highlightField = StateField.define({
provide: (f) => EditorView.decorations.from(f), provide: (f) => EditorView.decorations.from(f),
}); });
const staticExtensions = [javascript(), highlightField, flashField]; const staticExtensions = [
javascript(),
highlightField,
flashField,
// javascriptLanguage.data.of({ autocomplete: strudelAutocomplete }),
// autocompletion({ override: [strudelAutocomplete] }),
autocompletion({ override: [] }), // wait for https://github.com/uiwjs/react-codemirror/pull/458
];
export default function CodeMirror({ export default function CodeMirror({
value, value,
@@ -97,9 +100,6 @@ export default function CodeMirror({
onSelectionChange, onSelectionChange,
theme, theme,
keybindings, keybindings,
isLineNumbersDisplayed,
isAutoCompletionEnabled,
isLineWrappingEnabled,
fontSize = 18, fontSize = 18,
fontFamily = 'monospace', fontFamily = 'monospace',
options, options,
@@ -111,14 +111,12 @@ export default function CodeMirror({
}, },
[onChange], [onChange],
); );
const handleOnCreateEditor = useCallback( const handleOnCreateEditor = useCallback(
(view) => { (view) => {
onViewChanged?.(view); onViewChanged?.(view);
}, },
[onViewChanged], [onViewChanged],
); );
const handleOnUpdate = useCallback( const handleOnUpdate = useCallback(
(viewUpdate) => { (viewUpdate) => {
if (viewUpdate.selectionSet && onSelectionChange) { if (viewUpdate.selectionSet && onSelectionChange) {
@@ -127,31 +125,16 @@ export default function CodeMirror({
}, },
[onSelectionChange], [onSelectionChange],
); );
const extensions = useMemo(() => { const extensions = useMemo(() => {
let _extensions = [...staticExtensions];
let bindings = { let bindings = {
vim, vim,
emacs, emacs,
}; };
if (bindings[keybindings]) { if (bindings[keybindings]) {
_extensions.push(bindings[keybindings]()); return [...staticExtensions, bindings[keybindings]()];
} }
return staticExtensions;
if (isAutoCompletionEnabled) { }, [keybindings]);
_extensions.push(javascriptLanguage.data.of({ autocomplete: strudelAutocomplete }));
} else {
_extensions.push(autocompletion({ override: [] }));
}
if (isLineWrappingEnabled) {
_extensions.push(EditorView.lineWrapping);
}
return _extensions;
}, [keybindings, isAutoCompletionEnabled, isLineWrappingEnabled]);
return ( return (
<div style={{ fontSize, fontFamily }} className="w-full"> <div style={{ fontSize, fontFamily }} className="w-full">
<_CodeMirror <_CodeMirror
@@ -161,7 +144,6 @@ export default function CodeMirror({
onCreateEditor={handleOnCreateEditor} onCreateEditor={handleOnCreateEditor}
onUpdate={handleOnUpdate} onUpdate={handleOnUpdate}
extensions={extensions} extensions={extensions}
basicSetup={{ lineNumbers: isLineNumbersDisplayed }}
/> />
</div> </div>
); );
+28 -40
View File
@@ -18,24 +18,16 @@ export function MiniRepl({
tune, tune,
hideOutsideView = false, hideOutsideView = false,
enableKeyboard, enableKeyboard,
onTrigger,
drawTime, drawTime,
punchcard, punchcard,
punchcardLabels,
onPaint,
canvasHeight = 200, canvasHeight = 200,
fontSize = 18,
fontFamily,
hideHeader = false,
theme, theme,
keybindings,
isLineNumbersDisplayed,
}) { }) {
drawTime = drawTime || (punchcard ? [0, 4] : undefined); drawTime = drawTime || (punchcard ? [0, 4] : undefined);
const evalOnMount = !!drawTime; const evalOnMount = !!drawTime;
const drawContext = useCallback( const drawContext = useCallback(
punchcard ? (canvasId) => document.querySelector('#' + canvasId)?.getContext('2d') : null, !!drawTime ? (canvasId) => document.querySelector('#' + canvasId)?.getContext('2d') : null,
[punchcard], [drawTime],
); );
const { const {
code, code,
@@ -55,18 +47,7 @@ export function MiniRepl({
} = useStrudel({ } = useStrudel({
initialCode: tune, initialCode: tune,
defaultOutput: webaudioOutput, defaultOutput: webaudioOutput,
editPattern: (pat, id) => { editPattern: (pat) => (punchcard ? pat.punchcard() : pat),
//pat = pat.withContext((ctx) => ({ ...ctx, id }));
if (onTrigger) {
pat = pat.onTrigger(onTrigger, false);
}
if (onPaint) {
pat = pat.onPaint(onPaint);
} else if (punchcard) {
pat = pat.punchcard({ labels: punchcardLabels });
}
return pat;
},
getTime, getTime,
evalOnMount, evalOnMount,
drawContext, drawContext,
@@ -101,7 +82,7 @@ export function MiniRepl({
e.preventDefault(); e.preventDefault();
flash(view); flash(view);
await activateCode(); await activateCode();
} else if (e.key === '.' || e.code === 'Period') { } else if (e.code === 'Period') {
stop(); stop();
e.preventDefault(); e.preventDefault();
} }
@@ -112,6 +93,26 @@ export function MiniRepl({
), ),
); );
// set active pattern on ctrl+enter
useLayoutEffect(() => {
if (enableKeyboard) {
const handleKeyPress = async (e) => {
if (e.ctrlKey || e.altKey) {
if (e.code === 'Enter') {
e.preventDefault();
flash(view);
await activateCode();
} else if (e.code === 'Period') {
stop();
e.preventDefault();
}
}
};
window.addEventListener('keydown', handleKeyPress, true);
return () => window.removeEventListener('keydown', handleKeyPress, true);
}
}, [enableKeyboard, pattern, code, evaluate, stop, view]);
const [log, setLog] = useState([]); const [log, setLog] = useState([]);
useLogger( useLogger(
useCallback((e) => { useCallback((e) => {
@@ -120,7 +121,7 @@ export function MiniRepl({
// const logId = data?.pattern?.meta?.id; // const logId = data?.pattern?.meta?.id;
if (logId === replId) { if (logId === replId) {
setLog((l) => { setLog((l) => {
return l.concat([e.detail]).slice(-8); return l.concat([e.detail]).slice(-10);
}); });
} }
}, []), }, []),
@@ -128,7 +129,6 @@ export function MiniRepl({
return ( return (
<div className="overflow-hidden rounded-t-md bg-background border border-lineHighlight" ref={ref}> <div className="overflow-hidden rounded-t-md bg-background border border-lineHighlight" ref={ref}>
{!hideHeader && (
<div className="flex justify-between bg-lineHighlight"> <div className="flex justify-between bg-lineHighlight">
<div className="flex"> <div className="flex">
<button <button
@@ -150,24 +150,12 @@ export function MiniRepl({
<Icon type="refresh" /> <Icon type="refresh" />
</button> </button>
</div> </div>
{error && <div className="text-right p-1 text-sm text-red-200">{error.message}</div>}
</div> </div>
)}
<div className="overflow-auto relative"> <div className="overflow-auto relative">
{show && ( {show && <CodeMirror6 value={code} onChange={setCode} onViewChanged={setView} theme={theme} />}
<CodeMirror6
value={code}
onChange={setCode}
onViewChanged={setView}
theme={theme}
fontFamily={fontFamily}
fontSize={fontSize}
keybindings={keybindings}
isLineNumbersDisplayed={isLineNumbersDisplayed}
/>
)}
{error && <div className="text-right p-1 text-md text-red-200">{error.message}</div>}
</div> </div>
{punchcard && ( {drawTime && (
<canvas <canvas
id={canvasId} id={canvasId}
className="w-full pointer-events-none" className="w-full pointer-events-none"
+1 -5
View File
@@ -1,6 +1,6 @@
:root { :root {
--background: #222; --background: #222;
--lineBackground: #22222299; --lineBackground: #22222250;
--foreground: #fff; --foreground: #fff;
--caret: #ffcc00; --caret: #ffcc00;
--selection: rgba(128, 203, 196, 0.5); --selection: rgba(128, 203, 196, 0.5);
@@ -24,7 +24,3 @@
.cm-theme-light { .cm-theme-light {
width: 100%; width: 100%;
} }
footer {
z-index: 0 !important;
}
+1 -1
View File
@@ -17,7 +17,7 @@ function useHighlighting({ view, pattern, active, getTime }) {
const begin = Math.max(lastEnd.current ?? audioTime, audioTime - 1 / 10, -0.01); // negative time seems buggy const begin = Math.max(lastEnd.current ?? audioTime, audioTime - 1 / 10, -0.01); // negative time seems buggy
const span = [round(begin), round(audioTime + 1 / 60)]; const span = [round(begin), round(audioTime + 1 / 60)];
lastEnd.current = span[1]; lastEnd.current = span[1];
highlights.current = highlights.current.filter((hap) => hap.endClipped > audioTime); // keep only highlights that are still active highlights.current = highlights.current.filter((hap) => hap.whole.end > audioTime); // keep only highlights that are still active
const haps = pattern.queryArc(...span).filter((hap) => hap.hasOnset()); const haps = pattern.queryArc(...span).filter((hap) => hap.hasOnset());
highlights.current = highlights.current.concat(haps); // add potential new onsets highlights.current = highlights.current.concat(haps); // add potential new onsets
view.dispatch({ effects: setHighlights.of({ haps: highlights.current }) }); // highlight all still active + new active haps view.dispatch({ effects: setHighlights.of({ haps: highlights.current }) }); // highlight all still active + new active haps
+3 -4
View File
@@ -29,8 +29,7 @@ function useStrudel({
const [pattern, setPattern] = useState(); const [pattern, setPattern] = useState();
const [started, setStarted] = useState(false); const [started, setStarted] = useState(false);
const isDirty = code !== activeCode; const isDirty = code !== activeCode;
//const shouldPaint = useCallback((pat) => !!(pat?.context?.onPaint && drawContext), [drawContext]); const shouldPaint = useCallback((pat) => !!(pat?.context?.onPaint && drawContext), [drawContext]);
const shouldPaint = useCallback((pat) => !!pat?.context?.onPaint, []);
// TODO: make sure this hook reruns when scheduler.started changes // TODO: make sure this hook reruns when scheduler.started changes
const { scheduler, evaluate, start, stop, pause, setCps } = useMemo( const { scheduler, evaluate, start, stop, pause, setCps } = useMemo(
@@ -47,9 +46,9 @@ function useStrudel({
drawContext, drawContext,
transpiler, transpiler,
editPattern, editPattern,
beforeEval: async ({ code }) => { beforeEval: ({ code }) => {
setCode(code); setCode(code);
await beforeEval?.(); beforeEval?.();
}, },
afterEval: (res) => { afterEval: (res) => {
const { pattern: _pattern, code } = res; const { pattern: _pattern, code } = res;
+3 -2
View File
@@ -1,9 +1,10 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom';
import App from './App'; import App from './App';
import { createRoot } from 'react-dom/client';
createRoot(document.getElementById('root')).render( ReactDOM.render(
<React.StrictMode> <React.StrictMode>
<App /> <App />
</React.StrictMode>, </React.StrictMode>,
document.getElementById('root'),
); );
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel.cycles/serial", "name": "@strudel.cycles/serial",
"version": "0.8.0", "version": "0.6.0",
"description": "Webserial API for strudel", "description": "Webserial API for strudel",
"main": "serial.mjs", "main": "serial.mjs",
"publishConfig": { "publishConfig": {
@@ -32,6 +32,6 @@
"@strudel.cycles/core": "workspace:*" "@strudel.cycles/core": "workspace:*"
}, },
"devDependencies": { "devDependencies": {
"vite": "^4.3.3" "vite": "^3.2.2"
} }
} }
+7 -46
View File
@@ -1,6 +1,4 @@
import { noteToMidi, freqToMidi } from '@strudel.cycles/core'; import { toMidi } from '@strudel.cycles/core';
import { getAudioContext, registerSound, getEnvelope } from '@strudel.cycles/webaudio';
import gm from './gm.mjs';
let loadCache = {}; let loadCache = {};
async function loadFont(name) { async function loadFont(name) {
@@ -10,6 +8,7 @@ async function loadFont(name) {
const load = async () => { const load = async () => {
// TODO: make soundfont source configurable // TODO: make soundfont source configurable
const url = `https://felixroos.github.io/webaudiofontdata/sound/${name}.js`; const url = `https://felixroos.github.io/webaudiofontdata/sound/${name}.js`;
console.log('load font', name, url);
const preset = await fetch(url).then((res) => res.text()); const preset = await fetch(url).then((res) => res.text());
let [_, data] = preset.split('={'); let [_, data] = preset.split('={');
return eval('{' + data); return eval('{' + data);
@@ -18,24 +17,15 @@ async function loadFont(name) {
return loadCache[name]; return loadCache[name];
} }
export async function getFontBufferSource(name, value, ac) { export async function getFontBufferSource(name, pitch, ac) {
let { note = 'c3', freq } = value; if (typeof pitch === 'string') {
let midi; pitch = toMidi(pitch);
if (freq) {
midi = freqToMidi(freq);
} else if (typeof note === 'string') {
midi = noteToMidi(note);
} else if (typeof note === 'number') {
midi = note;
} else {
throw new Error(`unexpected "note" type "${typeof note}"`);
} }
const { buffer, zone } = await getFontPitch(name, pitch, ac);
const { buffer, zone } = await getFontPitch(name, midi, ac);
const src = ac.createBufferSource(); const src = ac.createBufferSource();
src.buffer = buffer; src.buffer = buffer;
const baseDetune = zone.originalPitch - 100.0 * zone.coarseTune - zone.fineTune; const baseDetune = zone.originalPitch - 100.0 * zone.coarseTune - zone.fineTune;
const playbackRate = 1.0 * Math.pow(2, (100.0 * midi - baseDetune) / 1200.0); const playbackRate = 1.0 * Math.pow(2, (100.0 * pitch - baseDetune) / 1200.0);
// src detune? // src detune?
src.playbackRate.value = playbackRate; src.playbackRate.value = playbackRate;
const loop = zone.loopStart > 1 && zone.loopStart < zone.loopEnd; const loop = zone.loopStart > 1 && zone.loopStart < zone.loopEnd;
@@ -124,32 +114,3 @@ async function getBuffer(zone, audioContext) {
} }
} }
} }
export function registerSoundfonts() {
Object.entries(gm).forEach(([name, fonts]) => {
registerSound(
name,
async (time, value, onended) => {
const { n = 0 } = value;
const { attack = 0.001, decay = 0.001, sustain = 1, release = 0.001 } = value;
const font = fonts[n % fonts.length];
const ctx = getAudioContext();
const bufferSource = await getFontBufferSource(font, value, ctx);
bufferSource.start(time);
const { node: envelope, stop: releaseEnvelope } = getEnvelope(attack, decay, sustain, release, 0.3, time);
bufferSource.connect(envelope);
const stop = (releaseTime) => {
bufferSource.stop(releaseTime + release);
releaseEnvelope(releaseTime);
};
bufferSource.onended = () => {
bufferSource.disconnect();
envelope.disconnect();
onended();
};
return { node: envelope, stop };
},
{ type: 'soundfont', prebake: true, fonts },
);
});
}
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -1,6 +1,6 @@
import { getFontBufferSource, registerSoundfonts } from './fontloader.mjs'; import { getFontBufferSource } from './fontloader.mjs';
import * as soundfontList from './list.mjs'; import * as soundfontList from './list.mjs';
import { startPresetNote } from 'sfumato'; import { startPresetNote } from 'sfumato';
import { loadSoundfont } from './sfumato.mjs'; import { loadSoundfont } from './sfumato.mjs';
export { loadSoundfont, startPresetNote, getFontBufferSource, soundfontList, registerSoundfonts }; export { loadSoundfont, startPresetNote, getFontBufferSource, soundfontList };
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel.cycles/soundfonts", "name": "@strudel.cycles/soundfonts",
"version": "0.8.2", "version": "0.6.0",
"description": "Soundsfont support for strudel", "description": "Soundsfont support for strudel",
"main": "index.mjs", "main": "index.mjs",
"publishConfig": { "publishConfig": {
@@ -36,7 +36,7 @@
"soundfont2": "^0.4.0" "soundfont2": "^0.4.0"
}, },
"devDependencies": { "devDependencies": {
"node-fetch": "^3.3.1", "node-fetch": "^3.2.6",
"vite": "^4.3.3" "vite": "^3.2.2"
} }
} }
+3 -27
View File
@@ -1,5 +1,5 @@
import { Pattern, getPlayableNoteValue, noteToMidi } from '@strudel.cycles/core'; import { Pattern, getPlayableNoteValue, toMidi } from '@strudel.cycles/core';
import { getAudioContext, registerSound } from '@strudel.cycles/webaudio'; import { getAudioContext } from '@strudel.cycles/webaudio';
import { loadSoundfont as _loadSoundfont, startPresetNote } from 'sfumato'; import { loadSoundfont as _loadSoundfont, startPresetNote } from 'sfumato';
Pattern.prototype.soundfont = function (sf, n = 0) { Pattern.prototype.soundfont = function (sf, n = 0) {
@@ -8,7 +8,7 @@ Pattern.prototype.soundfont = function (sf, n = 0) {
const note = getPlayableNoteValue(h); const note = getPlayableNoteValue(h);
const preset = sf.presets[n % sf.presets.length]; const preset = sf.presets[n % sf.presets.length];
const deadline = ctx.currentTime + t - ct; const deadline = ctx.currentTime + t - ct;
const args = [ctx, preset, noteToMidi(note), deadline]; const args = [ctx, preset, toMidi(note), deadline];
const stop = startPresetNote(...args); const stop = startPresetNote(...args);
stop(deadline + h.duration); stop(deadline + h.duration);
}); });
@@ -21,29 +21,5 @@ export function loadSoundfont(url) {
} }
const sf = _loadSoundfont(url); const sf = _loadSoundfont(url);
soundfontCache.set(url, sf); soundfontCache.set(url, sf);
/*sf.then((font) => {
font.presets.forEach((preset) => {
console.log('preset', preset.header.name);
registerSound(
preset.header.name.replaceAll(' ', '_'),
(time, value, onended) => {
const ctx = getAudioContext();
let { note } = value; // freq ?
const p = font.presets.find((p) => p.header.name === preset.header.name);
if (!p) {
throw new Error('preset not found');
}
const deadline = time; // - ctx.currentTime;
const args = [ctx, p, noteToMidi(note), deadline];
const stop = startPresetNote(...args);
return { node: undefined, stop };
},
{ type: 'soundfont' },
);
});
//console.log('f', f);
});*/
return sf; return sf;
} }
+4 -4
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel.cycles/tonal", "name": "@strudel.cycles/tonal",
"version": "0.8.2", "version": "0.6.0",
"description": "Tonal functions for strudel", "description": "Tonal functions for strudel",
"main": "index.mjs", "main": "index.mjs",
"publishConfig": { "publishConfig": {
@@ -34,10 +34,10 @@
"@strudel.cycles/core": "workspace:*", "@strudel.cycles/core": "workspace:*",
"@tonaljs/tonal": "^4.7.2", "@tonaljs/tonal": "^4.7.2",
"chord-voicings": "^0.0.1", "chord-voicings": "^0.0.1",
"webmidi": "^3.1.5" "webmidi": "^3.0.21"
}, },
"devDependencies": { "devDependencies": {
"vite": "^4.3.3", "vite": "^3.2.2",
"vitest": "^0.28.0" "vitest": "^0.25.7"
} }
} }
+1 -1
View File
@@ -145,7 +145,7 @@ export const scaleTranspose = register('scaleTranspose', function (offset /* : n
export const scale = register('scale', function (scale, pat) { export const scale = register('scale', function (scale, pat) {
// Supports ':' list syntax in mininotation // Supports ':' list syntax in mininotation
if (Array.isArray(scale)) { if (Array.isArray(scale)) {
scale = scale.flat().join(' '); scale = scale.join(' ');
} }
return pat.withHap((hap) => { return pat.withHap((hap) => {
const isObject = typeof hap.value === 'object'; const isObject = typeof hap.value === 'object';
+1 -44
View File
@@ -6,53 +6,10 @@ This program is free software: you can redistribute it and/or modify it under th
import { stack, register } from '@strudel.cycles/core'; import { stack, register } from '@strudel.cycles/core';
import _voicings from 'chord-voicings'; import _voicings from 'chord-voicings';
const { dictionaryVoicing, minTopNoteDiff } = _voicings.default || _voicings; // parcel module resolution fuckup const { dictionaryVoicing, minTopNoteDiff, lefthand } = _voicings.default || _voicings; // parcel module resolution fuckup
const lefthand = {
m7: ['3m 5P 7m 9M', '7m 9M 10m 12P'],
7: ['3M 6M 7m 9M', '7m 9M 10M 13M'],
'^7': ['3M 5P 7M 9M', '7M 9M 10M 12P'],
69: ['3M 5P 6A 9M'],
m7b5: ['3m 5d 7m 8P', '7m 8P 10m 12d'],
'7b9': ['3M 6m 7m 9m', '7m 9m 10M 13m'],
'7b13': ['3M 6m 7m 9m', '7m 9m 10M 13m'],
o7: ['1P 3m 5d 6M', '5d 6M 8P 10m'],
'7#11': ['7m 9M 11A 13A'],
'7#9': ['3M 7m 9A'],
mM7: ['3m 5P 7M 9M', '7M 9M 10m 12P'],
m6: ['3m 5P 6M 9M', '6M 9M 10m 12P'],
};
const guidetones = {
m7: ['3m 7m', '7m 10m'],
m9: ['3m 7m', '7m 10m'],
7: ['3M 7m', '7m 10M'],
'^7': ['3M 7M', '7M 10M'],
'^9': ['3M 7M', '7M 10M'],
69: ['3M 6M'],
6: ['3M 6M', '6M 10M'],
m7b5: ['3m 7m', '7m 10m'],
'7b9': ['3M 7m', '7m 10M'],
'7b13': ['3M 7m', '7m 10M'],
o7: ['3m 6M', '6M 10m'],
'7#11': ['3M 7m', '7m 10M'],
'7#9': ['3M 7m', '7m 10M'],
mM7: ['3m 7M', '7M 10m'],
m6: ['3m 6M', '6M 10m'],
};
const triads = {
'': ['1P 3M 5P', '3M 5P 8P', '5P 8P 10M'],
M: ['1P 3M 5P', '3M 5P 8P', '5P 8P 10M'],
m: ['1P 3m 5P', '3m 5P 8P', '5P 8P 10m'],
o: ['1P 3m 5d', '3m 5d 8P', '5d 8P 10m'],
aug: ['1P 3m 5A', '3m 5A 8P', '5A 8P 10m'],
};
export const voicingRegistry = { export const voicingRegistry = {
lefthand: { dictionary: lefthand, range: ['F3', 'A4'] }, lefthand: { dictionary: lefthand, range: ['F3', 'A4'] },
triads: { dictionary: triads },
guidetones: { dictionary: guidetones },
}; };
export const setVoicingRange = (name, range) => addVoicings(name, voicingRegistry[name].dictionary, range); export const setVoicingRange = (name, range) => addVoicings(name, voicingRegistry[name].dictionary, range);
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel.cycles/tone", "name": "@strudel.cycles/tone",
"version": "0.8.0", "version": "0.6.0",
"description": "Tone.js API for strudel", "description": "Tone.js API for strudel",
"main": "index.mjs", "main": "index.mjs",
"type": "module", "type": "module",
@@ -34,7 +34,7 @@
"tone": "^14.7.77" "tone": "^14.7.77"
}, },
"devDependencies": { "devDependencies": {
"vite": "^4.3.3", "vite": "^3.2.2",
"vitest": "^0.28.0" "vitest": "^0.25.7"
} }
} }
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel.cycles/transpiler", "name": "@strudel.cycles/transpiler",
"version": "0.8.2", "version": "0.6.0",
"description": "Transpiler for strudel user code. Converts syntactically correct but semantically meaningless JS into evaluatable strudel code.", "description": "Transpiler for strudel user code. Converts syntactically correct but semantically meaningless JS into evaluatable strudel code.",
"main": "index.mjs", "main": "index.mjs",
"publishConfig": { "publishConfig": {
@@ -36,7 +36,7 @@
"estree-walker": "^3.0.1" "estree-walker": "^3.0.1"
}, },
"devDependencies": { "devDependencies": {
"vite": "^4.3.3", "vite": "^3.2.2",
"vitest": "^0.28.0" "vitest": "^0.25.8"
} }
} }
-82
View File
@@ -1,82 +0,0 @@
# @strudel/web
This package provides an easy to use bundle of multiple strudel packages for the web.
## Usage
Save this code as a `.html` file and double click it:
```html
<!DOCTYPE html>
<button id="play">play</button>
<button id="stop">stop</button>
<script type="module">
import { initStrudel } from 'https://cdn.skypack.dev/@strudel/web@0.8.2';
initStrudel();
document.getElementById('play').addEventListener('click', () => note('<c a f e>(3,8)').play());
document.getElementById('stop').addEventListener('click', () => hush());
</script>
```
With the help of [skypack](https://www.skypack.dev/), you don't need a bundler nor a server.
As soon as you call `initStrudel()`, all strudel functions are made available.
In this case, we are using the `note` function to create a pattern.
To actually play the pattern, you have to append `.play()` to the end.
Note: Due to the [Autoplay policy](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Best_practices#autoplay_policy), you can only play audio in a browser after a click event.
### Via npm
If you're using a bundler, you can install the package via `npm i @strudel/web`, then just import it like:
```js
import { initStrudel } from '@strudel/web';
```
The rest of the code should be the same. Check out [vite](https://vitejs.dev/) for a good bundler / dev server.
### Loading samples
By default, no external samples are loaded, but you can add them like this:
```js
initStrudel({
prebake: () => samples('github:tidalcycles/Dirt-Samples/master'),
});
document.getElementById('play').addEventListener('click',
() => s("bd sd").play()
)
```
You can learn [more about the `samples` function here](https://strudel.tidalcycles.org/learn/samples#loading-custom-samples).
### Evaluating Code
Instead of creating patterns directly in JS, you might also want to take in user input and turn that into a pattern.
This is called evaluation: Taking a piece of code and executing it on the fly.
To do that, you can use the `evaluate` function:
```js
initStrudel();
document.getElementById('play').addEventListener('click',
() => evaluate('note("c a f e").jux(rev)')
);
document.getElementById('play').addEventListener('stop',
() => hush()
);
```
### Double vs Single Quotes
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.
## More Examples
Check out the examples folder for more examples, both using plain html and vite!
-10
View File
@@ -1,10 +0,0 @@
<!DOCTYPE html>
<button id="play">play</button>
<button id="stop">stop</button>
<script type="module">
import { initStrudel } from 'https://cdn.skypack.dev/@strudel/web@0.8.2';
initStrudel();
document.getElementById('play').addEventListener('click', () => evaluate('note("c a f e").jux(rev)'));
document.getElementById('play').addEventListener('stop', () => hush());
</script>
@@ -1,16 +0,0 @@
<!DOCTYPE html>
<button id="a">A</button>
<button id="b">B</button>
<button id="c">C</button>
<button id="stop">stop</button>
<script type="module">
import { initStrudel } from 'https://cdn.skypack.dev/@strudel/web@0.8.2';
initStrudel({
prebake: () => samples('github:tidalcycles/Dirt-Samples/master'),
});
const click = (id, action) => document.getElementById(id).addEventListener('click', action);
click('a', () => evaluate(`s('bd,jvbass(3,8)').jux(rev)`));
click('b', () => s('bd*2,hh(3,4),jvbass(5,8,1)').jux(rev).play());
click('c', () => s('bd*2,hh(3,4),jvbass:[0 4](5,8,1)').jux(rev).stack(s('~ sd')).play());
click('stop', () => hush());
</script>
-10
View File
@@ -1,10 +0,0 @@
<!DOCTYPE html>
<button id="play">play</button>
<button id="stop">stop</button>
<script type="module">
import { initStrudel } from 'https://cdn.skypack.dev/@strudel/web@0.8.2';
initStrudel();
document.getElementById('play').addEventListener('click', () => note('<c a f e>(3,8)').play());
document.getElementById('stop').addEventListener('click', () => hush());
</script>
@@ -1,24 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
@@ -1,28 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<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>
<body>
<div id="app"></div>
<button id="a">A</button>
<button id="b">B</button>
<button id="c">C</button>
<button id="stop">stop</button>
<script type="module">
import { initStrudel } from '@strudel/web';
initStrudel({
prebake: () => samples('github:tidalcycles/Dirt-Samples/master'),
});
const click = (id, action) => document.getElementById(id).addEventListener('click', action);
click('a', () => evaluate(`s('bd,jvbass(3,8)').jux(rev)`));
click('b', () => s('bd*2,hh(3,4),jvbass(5,8,1)').jux(rev).play());
click('c', () => s('bd*2,hh(3,4),jvbass:[0 4](5,8,1)').jux(rev).stack(s('~ sd')).play());
click('stop', () => hush());
</script>
</body>
</html>
@@ -1,18 +0,0 @@
{
"name": "repl-example",
"private": true,
"version": "0.0.0",
"type": "module",
"license": "AGPL-3.0-or-later",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"vite": "^4.3.2"
},
"dependencies": {
"@strudel/web": "workspace:*"
}
}
-12
View File
@@ -1,12 +0,0 @@
<!DOCTYPE html>
<button id="play">play</button>
<button id="stop">stop</button>
<script type="module">
import { initStrudel } from 'https://cdn.skypack.dev/@strudel/web@0.8.2';
initStrudel({
prebake: () => samples('github:tidalcycles/Dirt-Samples/master'),
});
document.getElementById('play').addEventListener('click', () => s('[bd sd](3,8)').play());
document.getElementById('stop').addEventListener('click', () => hush());
</script>
-45
View File
@@ -1,45 +0,0 @@
{
"name": "@strudel/web",
"version": "0.8.3",
"description": "Easy to setup, opiniated bundle of Strudel for the browser.",
"main": "web.mjs",
"publishConfig": {
"main": "dist/index.js",
"module": "dist/index.mjs"
},
"scripts": {
"build": "vite build",
"prepublishOnly": "npm run build"
},
"type": "module",
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
},
"keywords": [
"tidalcycles",
"strudel",
"pattern",
"livecoding",
"algorave"
],
"author": "Felix Roos <flix91@gmail.com>",
"contributors": [
"Alex McLean <alex@slab.org>"
],
"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:*",
"@strudel.cycles/mini": "workspace:*",
"@strudel.cycles/tonal": "workspace:*",
"@strudel.cycles/transpiler": "workspace:*",
"@strudel.cycles/webaudio": "workspace:*"
},
"devDependencies": {
"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, 'web.mjs'),
formats: ['es', 'cjs'],
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
},
rollupOptions: {
external: [...Object.keys(dependencies)],
},
target: 'esnext',
},
});
-66
View File
@@ -1,66 +0,0 @@
export * from '@strudel.cycles/core';
export * from '@strudel.cycles/webaudio';
//export * from '@strudel.cycles/soundfonts';
export * from '@strudel.cycles/transpiler';
export * from '@strudel.cycles/mini';
export * from '@strudel.cycles/tonal';
export * from '@strudel.cycles/webaudio';
import { Pattern, evalScope, controls } from '@strudel.cycles/core';
import { initAudioOnFirstClick, registerSynthSounds, webaudioScheduler } from '@strudel.cycles/webaudio';
// import { registerSoundfonts } from '@strudel.cycles/soundfonts';
import { evaluate as _evaluate } from '@strudel.cycles/transpiler';
import { miniAllStrings } from '@strudel.cycles/mini';
// init logic
export async function defaultPrebake() {
const loadModules = evalScope(
evalScope,
controls,
import('@strudel.cycles/core'),
import('@strudel.cycles/mini'),
import('@strudel.cycles/tonal'),
import('@strudel.cycles/webaudio'),
{ hush, evaluate },
);
await Promise.all([loadModules, registerSynthSounds() /* , registerSoundfonts() */]);
}
// when this function finishes, everything is initialized
let initDone;
let scheduler;
export function initStrudel(options = {}) {
initAudioOnFirstClick();
miniAllStrings();
const { prebake, ...schedulerOptions } = options;
initDone = (async () => {
await defaultPrebake();
await prebake?.();
})();
scheduler = webaudioScheduler(schedulerOptions);
}
window.initStrudel = initStrudel;
// this method will play the pattern on the default scheduler
Pattern.prototype.play = function () {
if (!scheduler) {
throw new Error('.play: no scheduler found. Have you called init?');
}
initDone.then(() => {
scheduler.setPattern(this, true);
});
return this;
};
// stop playback
export function hush() {
scheduler.stop();
}
// evaluate and play the given code using the transpiler
export async function evaluate(code, autoplay = true) {
const { pattern } = await _evaluate(code);
autoplay && pattern.play();
}
-70
View File
@@ -1,70 +0,0 @@
import { getAudioContext } from './webaudio.mjs';
export function gainNode(value) {
const node = getAudioContext().createGain();
node.gain.value = value;
return node;
}
export const getOscillator = ({ s, freq, t }) => {
// make oscillator
const o = getAudioContext().createOscillator();
o.type = s || 'triangle';
o.frequency.value = Number(freq);
o.start(t);
//o.stop(t + duration + release);
const stop = (time) => o.stop(time);
return { node: o, stop };
};
// alternative to getADSR returning the gain node and a stop handle to trigger the release anytime in the future
export const getEnvelope = (attack, decay, sustain, release, velocity, begin) => {
const gainNode = getAudioContext().createGain();
gainNode.gain.setValueAtTime(0, begin);
gainNode.gain.linearRampToValueAtTime(velocity, begin + attack); // attack
gainNode.gain.linearRampToValueAtTime(sustain * velocity, begin + attack + decay); // sustain start
// sustain end
return {
node: gainNode,
stop: (t) => {
//if (typeof gainNode.gain.cancelAndHoldAtTime === 'function') {
// gainNode.gain.cancelAndHoldAtTime(t); // this seems to release instantly....
// see https://discord.com/channels/779427371270275082/937365093082079272/1086053607360712735
//} else {
// firefox: this will glitch when the sustain has not been reached yet at the time of release
gainNode.gain.setValueAtTime(sustain * velocity, t);
//}
gainNode.gain.linearRampToValueAtTime(0, t + release);
},
};
};
export const getADSR = (attack, decay, sustain, release, velocity, begin, end) => {
const gainNode = getAudioContext().createGain();
gainNode.gain.setValueAtTime(0, begin);
gainNode.gain.linearRampToValueAtTime(velocity, begin + attack); // attack
gainNode.gain.linearRampToValueAtTime(sustain * velocity, begin + attack + decay); // sustain start
gainNode.gain.setValueAtTime(sustain * velocity, end); // sustain end
gainNode.gain.linearRampToValueAtTime(0, end + release); // release
// for some reason, using exponential ramping creates little cracklings
/* let t = begin;
gainNode.gain.setValueAtTime(0, t);
gainNode.gain.exponentialRampToValueAtTime(velocity, (t += attack));
const sustainGain = Math.max(sustain * velocity, 0.001);
gainNode.gain.exponentialRampToValueAtTime(sustainGain, (t += decay));
if (end - begin < attack + decay) {
gainNode.gain.cancelAndHoldAtTime(end);
} else {
gainNode.gain.setValueAtTime(sustainGain, end);
}
gainNode.gain.exponentialRampToValueAtTime(0.001, end + release); // release */
return gainNode;
};
export const getFilter = (type, frequency, Q) => {
const filter = getAudioContext().createBiquadFilter();
filter.type = type;
filter.frequency.value = frequency;
filter.Q.value = Q;
return filter;
};
-2
View File
@@ -6,5 +6,3 @@ This program is free software: you can redistribute it and/or modify it under th
export * from './webaudio.mjs'; export * from './webaudio.mjs';
export * from './sampler.mjs'; export * from './sampler.mjs';
export * from './helpers.mjs';
export * from './synth.mjs';
+3 -4
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel.cycles/webaudio", "name": "@strudel.cycles/webaudio",
"version": "0.8.2", "version": "0.6.1",
"description": "Web Audio helpers for Strudel", "description": "Web Audio helpers for Strudel",
"main": "index.mjs", "main": "index.mjs",
"type": "module", "type": "module",
@@ -34,10 +34,9 @@
}, },
"homepage": "https://github.com/tidalcycles/strudel#readme", "homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": { "dependencies": {
"@strudel.cycles/core": "workspace:*", "@strudel.cycles/core": "workspace:*"
"nanostores": "^0.8.1"
}, },
"devDependencies": { "devDependencies": {
"vite": "^4.3.3" "vite": "^3.2.2"
} }
} }
+54 -159
View File
@@ -1,6 +1,5 @@
import { logger, noteToMidi, valueToMidi } from '@strudel.cycles/core'; import { logger, toMidi, valueToMidi } from '@strudel.cycles/core';
import { getAudioContext, registerSound } from './index.mjs'; import { getAudioContext } from './index.mjs';
import { getEnvelope } from './helpers.mjs';
const bufferCache = {}; // string: Promise<ArrayBuffer> const bufferCache = {}; // string: Promise<ArrayBuffer>
const loadCache = {}; // string: Promise<ArrayBuffer> const loadCache = {}; // string: Promise<ArrayBuffer>
@@ -21,7 +20,7 @@ function humanFileSize(bytes, si) {
return bytes.toFixed(1) + ' ' + units[u]; return bytes.toFixed(1) + ' ' + units[u];
} }
export const getSampleBufferSource = async (s, n, note, speed, freq, bank, resolveUrl) => { export const getSampleBufferSource = async (s, n, note, speed, freq) => {
let transpose = 0; let transpose = 0;
if (freq !== undefined && note !== undefined) { if (freq !== undefined && note !== undefined) {
logger('[sampler] hap has note and freq. ignoring note', 'warning'); logger('[sampler] hap has note and freq. ignoring note', 'warning');
@@ -30,11 +29,28 @@ export const getSampleBufferSource = async (s, n, note, speed, freq, bank, resol
transpose = midi - 36; // C3 is middle C transpose = midi - 36; // C3 is middle C
const ac = getAudioContext(); const ac = getAudioContext();
// is sample from loaded samples(..)
const samples = getLoadedSamples();
if (!samples) {
throw new Error('no samples loaded');
}
const bank = samples?.[s];
if (!bank) {
throw new Error(
`sample not found: "${s}"`,
// , try one of ${Object.keys(samples)
// .map((s) => `"${s}"`)
// .join(', ')}.
);
}
if (typeof bank !== 'object') {
throw new Error('wrong format for sample bank:', s);
}
let sampleUrl; let sampleUrl;
if (Array.isArray(bank)) { if (Array.isArray(bank)) {
sampleUrl = bank[n % bank.length]; sampleUrl = bank[n % bank.length];
} else { } else {
const midiDiff = (noteA) => noteToMidi(noteA) - midi; const midiDiff = (noteA) => toMidi(noteA) - midi;
// object format will expect keys as notes // object format will expect keys as notes
const closest = Object.keys(bank) const closest = Object.keys(bank)
.filter((k) => !k.startsWith('_')) .filter((k) => !k.startsWith('_'))
@@ -45,9 +61,6 @@ export const getSampleBufferSource = async (s, n, note, speed, freq, bank, resol
transpose = -midiDiff(closest); // semitones to repitch transpose = -midiDiff(closest); // semitones to repitch
sampleUrl = bank[closest][n % bank[closest].length]; sampleUrl = bank[closest][n % bank[closest].length];
} }
if (resolveUrl) {
sampleUrl = await resolveUrl(sampleUrl);
}
let buffer = await loadBuffer(sampleUrl, ac, s, n); let buffer = await loadBuffer(sampleUrl, ac, s, n);
if (speed < 0) { if (speed < 0) {
// should this be cached? // should this be cached?
@@ -94,45 +107,7 @@ export const getLoadedBuffer = (url) => {
return bufferCache[url]; return bufferCache[url];
}; };
export const processSampleMap = (sampleMap, fn, baseUrl = sampleMap._base || '') => { let sampleCache = { current: undefined };
return Object.entries(sampleMap).forEach(([key, value]) => {
if (typeof value === 'string') {
value = [value];
}
if (typeof value !== 'object') {
throw new Error('wrong sample map format for ' + key);
}
baseUrl = value._base || baseUrl;
const replaceUrl = (v) => (baseUrl + v).replace('github:', 'https://raw.githubusercontent.com/');
if (Array.isArray(value)) {
//return [key, value.map(replaceUrl)];
value = value.map(replaceUrl);
} else {
// must be object
value = Object.fromEntries(
Object.entries(value).map(([note, samples]) => {
return [note, (typeof samples === 'string' ? [samples] : samples).map(replaceUrl)];
}),
);
}
fn(key, value);
});
};
// allows adding a custom url prefix handler
// for example, it is used by the desktop app to load samples starting with '~/music'
let resourcePrefixHandlers = {};
export function registerSamplesPrefix(prefix, resolve) {
resourcePrefixHandlers[prefix] = resolve;
}
// finds a prefix handler for the given url (if any)
function getSamplesPrefixHandler(url) {
const handler = Object.entries(resourcePrefixHandlers).find(([key]) => url.startsWith(key));
if (handler) {
return handler[1];
}
return;
}
/** /**
* Loads a collection of samples to use with `s` * Loads a collection of samples to use with `s`
@@ -148,13 +123,8 @@ function getSamplesPrefixHandler(url) {
* *
*/ */
export const samples = async (sampleMap, baseUrl = sampleMap._base || '', options = {}) => { export const samples = async (sampleMap, baseUrl = sampleMap._base || '') => {
if (typeof sampleMap === 'string') { if (typeof sampleMap === 'string') {
// check if custom prefix handler
const handler = getSamplesPrefixHandler(sampleMap);
if (handler) {
return handler(sampleMap);
}
if (sampleMap.startsWith('github:')) { if (sampleMap.startsWith('github:')) {
let [_, path] = sampleMap.split('github:'); let [_, path] = sampleMap.split('github:');
path = path.endsWith('/') ? path.slice(0, -1) : path; path = path.endsWith('/') ? path.slice(0, -1) : path;
@@ -171,118 +141,43 @@ export const samples = async (sampleMap, baseUrl = sampleMap._base || '', option
} }
return fetch(sampleMap) return fetch(sampleMap)
.then((res) => res.json()) .then((res) => res.json())
.then((json) => samples(json, baseUrl || json._base || base, options)) .then((json) => samples(json, baseUrl || json._base || base))
.catch((error) => { .catch((error) => {
console.error(error); console.error(error);
throw new Error(`error loading "${sampleMap}"`); throw new Error(`error loading "${sampleMap}"`);
}); });
} }
const { prebake, tag } = options; sampleCache.current = {
processSampleMap( ...sampleCache.current,
sampleMap, ...Object.fromEntries(
(key, value) => Object.entries(sampleMap).map(([key, value]) => {
registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value), { if (typeof value === 'string') {
type: 'sample', value = [value];
samples: value, }
baseUrl, if (typeof value !== 'object') {
prebake, throw new Error('wrong sample map format for ' + key);
tag, }
baseUrl = value._base || baseUrl;
const replaceUrl = (v) => (baseUrl + v).replace('github:', 'https://raw.githubusercontent.com/');
if (Array.isArray(value)) {
return [key, value.map(replaceUrl)];
}
// must be object
return [
key,
Object.fromEntries(
Object.entries(value).map(([note, samples]) => {
return [note, (typeof samples === 'string' ? [samples] : samples).map(replaceUrl)];
}), }),
baseUrl, ),
); ];
}),
),
};
}; };
const cutGroups = []; export const resetLoadedSamples = () => {
sampleCache.current = undefined;
};
export async function onTriggerSample(t, value, onended, bank, resolveUrl) { export const getLoadedSamples = () => sampleCache.current;
const {
s,
freq,
unit,
nudge = 0, // TODO: is this in seconds?
cut,
loop,
clip = undefined, // if 1, samples will be cut off when the hap ends
n = 0,
note,
speed = 1, // sample playback speed
begin = 0,
end = 1,
} = value;
// load sample
if (speed === 0) {
// no playback
return;
}
const ac = getAudioContext();
// destructure adsr here, because the default should be different for synths and samples
const { attack = 0.001, decay = 0.001, sustain = 1, release = 0.001 } = value;
//const soundfont = getSoundfontKey(s);
const time = t + nudge;
const bufferSource = await getSampleBufferSource(s, n, note, speed, freq, bank, resolveUrl);
// asny stuff above took too long?
if (ac.currentTime > t) {
logger(`[sampler] still loading sound "${s}:${n}"`, 'highlight');
// console.warn('sample still loading:', s, n);
return;
}
if (!bufferSource) {
logger(`[sampler] could not load "${s}:${n}"`, 'error');
return;
}
bufferSource.playbackRate.value = Math.abs(speed) * bufferSource.playbackRate.value;
if (unit === 'c') {
// are there other units?
bufferSource.playbackRate.value = bufferSource.playbackRate.value * bufferSource.buffer.duration * 1; //cps;
}
// "The computation of the offset into the sound is performed using the sound buffer's natural sample rate,
// rather than the current playback rate, so even if the sound is playing at twice its normal speed,
// the midway point through a 10-second audio buffer is still 5."
const offset = begin * bufferSource.buffer.duration;
bufferSource.start(time, offset);
const bufferDuration = bufferSource.buffer.duration / bufferSource.playbackRate.value;
/*if (loop) {
// TODO: idea for loopBegin / loopEnd
// if one of [loopBegin,loopEnd] is <= 1, interpret it as normlized
// if [loopBegin,loopEnd] is bigger >= 1, interpret it as sample number
// this will simplify perfectly looping things, while still keeping the normalized option
// the only drawback is that looping between samples 0 and 1 is not possible (which is not real use case)
bufferSource.loop = true;
bufferSource.loopStart = offset;
bufferSource.loopEnd = offset + duration;
duration = loop * duration;
}*/
const { node: envelope, stop: releaseEnvelope } = getEnvelope(attack, decay, sustain, release, 1, t);
bufferSource.connect(envelope);
const out = ac.createGain(); // we need a separate gain for the cutgroups because firefox...
envelope.connect(out);
bufferSource.onended = function () {
bufferSource.disconnect();
envelope.disconnect();
out.disconnect();
onended();
};
const stop = (endTime, playWholeBuffer = clip === undefined) => {
let releaseTime = endTime;
if (playWholeBuffer) {
releaseTime = t + (end - begin) * bufferDuration;
}
bufferSource.stop(releaseTime + release);
releaseEnvelope(releaseTime);
};
const handle = { node: out, bufferSource, stop };
// cut groups
if (cut !== undefined) {
const prev = cutGroups[cut];
if (prev) {
prev.node.gain.setValueAtTime(1, time);
prev.node.gain.linearRampToValueAtTime(0, time + 0.01);
}
cutGroups[cut] = handle;
}
return handle;
}
-44
View File
@@ -1,44 +0,0 @@
import { midiToFreq, noteToMidi } from '@strudel.cycles/core';
import { registerSound } from './webaudio.mjs';
import { getOscillator, gainNode, getEnvelope } from './helpers.mjs';
export function registerSynthSounds() {
['sine', 'square', 'triangle', 'sawtooth'].forEach((wave) => {
registerSound(
wave,
(t, value, onended) => {
// destructure adsr here, because the default should be different for synths and samples
const { attack = 0.001, decay = 0.05, sustain = 0.6, release = 0.01 } = value;
let { n, note, freq } = value;
// with synths, n and note are the same thing
n = note || n || 36;
if (typeof n === 'string') {
n = noteToMidi(n); // e.g. c3 => 48
}
// get frequency
if (!freq && typeof n === 'number') {
freq = midiToFreq(n); // + 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 });
const g = gainNode(0.3);
// envelope
const { node: envelope, stop: releaseEnvelope } = getEnvelope(attack, decay, sustain, release, 1, t);
o.onended = () => {
o.disconnect();
g.disconnect();
onended();
};
return {
node: o.connect(g).connect(envelope),
stop: (releaseTime) => {
releaseEnvelope(releaseTime);
stop(releaseTime + release);
},
};
},
{ type: 'synth', prebake: true },
);
});
}
+163 -57
View File
@@ -5,22 +5,15 @@ This program is free software: you can redistribute it and/or modify it under th
*/ */
import * as strudel from '@strudel.cycles/core'; import * as strudel from '@strudel.cycles/core';
import { fromMidi, logger, toMidi } from '@strudel.cycles/core';
import './feedbackdelay.mjs'; import './feedbackdelay.mjs';
import './reverb.mjs'; import './reverb.mjs';
const { Pattern, logger } = strudel; import { getSampleBufferSource } from './sampler.mjs';
const { Pattern } = strudel;
import './vowel.mjs'; import './vowel.mjs';
import workletsUrl from './worklets.mjs?url'; import workletsUrl from './worklets.mjs?url';
import { getFilter, gainNode } from './helpers.mjs';
import { map } from 'nanostores';
export const soundMap = map(); // export const getAudioContext = () => Tone.getContext().rawContext;
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; let audioContext;
export const getAudioContext = () => { export const getAudioContext = () => {
@@ -45,6 +38,65 @@ export const panic = () => {
destination = null; destination = null;
}; };
const getFilter = (type, frequency, Q) => {
const filter = getAudioContext().createBiquadFilter();
filter.type = type;
filter.frequency.value = frequency;
filter.Q.value = Q;
return filter;
};
const getADSR = (attack, decay, sustain, release, velocity, begin, end) => {
const gainNode = getAudioContext().createGain();
gainNode.gain.setValueAtTime(0, begin);
gainNode.gain.linearRampToValueAtTime(velocity, begin + attack); // attack
gainNode.gain.linearRampToValueAtTime(sustain * velocity, begin + attack + decay); // sustain start
gainNode.gain.setValueAtTime(sustain * velocity, end); // sustain end
gainNode.gain.linearRampToValueAtTime(0, end + release); // release
// for some reason, using exponential ramping creates little cracklings
/* let t = begin;
gainNode.gain.setValueAtTime(0, t);
gainNode.gain.exponentialRampToValueAtTime(velocity, (t += attack));
const sustainGain = Math.max(sustain * velocity, 0.001);
gainNode.gain.exponentialRampToValueAtTime(sustainGain, (t += decay));
if (end - begin < attack + decay) {
gainNode.gain.cancelAndHoldAtTime(end);
} else {
gainNode.gain.setValueAtTime(sustainGain, end);
}
gainNode.gain.exponentialRampToValueAtTime(0.001, end + release); // release */
return gainNode;
};
const getOscillator = ({ s, freq, t, duration, release }) => {
// make oscillator
const o = getAudioContext().createOscillator();
o.type = s || 'triangle';
o.frequency.value = Number(freq);
o.start(t);
o.stop(t + duration + release);
return o;
};
const getSoundfontKey = (s) => {
if (!globalThis.soundfontList) {
// soundfont package not loaded
return false;
}
if (globalThis.soundfontList?.instruments?.includes(s)) {
return s;
}
// check if s is one of the soundfonts, which are loaded into globalThis, to avoid coupling both packages
const nameIndex = globalThis.soundfontList?.instrumentNames?.indexOf(s);
// convert number nameIndex (0-128) to 3 digit string (001-128)
const name = nameIndex < 10 ? `00${nameIndex}` : nameIndex < 100 ? `0${nameIndex}` : nameIndex;
if (nameIndex !== -1) {
// TODO: indices of instrumentNames do not seem to match instruments
return globalThis.soundfontList.instruments.find((instrument) => instrument.startsWith(name));
}
return;
};
let workletsLoading; let workletsLoading;
function loadWorklets() { function loadWorklets() {
if (workletsLoading) { if (workletsLoading) {
@@ -84,13 +136,15 @@ export async function initAudioOnFirstClick() {
}); });
} }
function gainNode(value) {
const node = getAudioContext().createGain();
node.gain.value = value;
return node;
}
const cutGroups = [];
let delays = {}; let delays = {};
const maxfeedback = 0.98;
function getDelay(orbit, delaytime, delayfeedback, t) { function getDelay(orbit, delaytime, delayfeedback, t) {
if (delayfeedback > maxfeedback) {
logger(`delayfeedback was clamped to ${maxfeedback} to save your ears`);
}
delayfeedback = strudel.clamp(delayfeedback, 0, 0.98);
if (!delays[orbit]) { if (!delays[orbit]) {
const ac = getAudioContext(); const ac = getAudioContext();
const dly = ac.createFeedbackDelay(1, delaytime, delayfeedback); const dly = ac.createFeedbackDelay(1, delaytime, delayfeedback);
@@ -132,11 +186,15 @@ export const webaudioOutput = async (hap, deadline, hapDuration, cps) => {
// calculate absolute time // calculate absolute time
let t = ac.currentTime + deadline; let t = ac.currentTime + deadline;
// destructure // destructure value
let { let {
s = 'triangle', freq,
s,
bank, bank,
source, sf,
clip = 0, // if 1, samples will be cut off when the hap ends
n = 0,
note,
gain = 0.8, gain = 0.8,
// low pass // low pass
cutoff, cutoff,
@@ -152,48 +210,110 @@ export const webaudioOutput = async (hap, deadline, hapDuration, cps) => {
crush, crush,
shape, shape,
pan, pan,
speed = 1, // sample playback speed
begin = 0,
end = 1,
vowel, vowel,
delay = 0, delay = 0,
delayfeedback = 0.5, delayfeedback = 0.5,
delaytime = 0.25, delaytime = 0.25,
unit,
nudge = 0, // TODO: is this in seconds?
cut,
loop,
orbit = 1, orbit = 1,
room, room,
size = 2, size = 2,
} = hap.value; } = hap.value;
const { velocity = 1 } = hap.context; const { velocity = 1 } = hap.context;
gain *= velocity; // legacy fix for velocity gain *= velocity; // legacy fix for velocity
let toDisconnect = []; // audio nodes that will be disconnected when the source has ended // the chain will hold all audio nodes that connect to each other
const onended = () => { const chain = [];
toDisconnect.forEach((n) => n?.disconnect());
};
if (bank && s) { if (bank && s) {
s = `${bank}_${s}`; s = `${bank}_${s}`;
} }
// get source AudioNode if (!s || ['sine', 'square', 'triangle', 'sawtooth'].includes(s)) {
let sourceNode; // destructure adsr here, because the default should be different for synths and samples
if (source) { const { attack = 0.001, decay = 0.05, sustain = 0.6, release = 0.01 } = hap.value;
sourceNode = source(t, hap.value, hapDuration); // with synths, n and note are the same thing
} else if (getSound(s)) { n = note || n || 36;
const { onTrigger } = getSound(s); if (typeof n === 'string') {
const soundHandle = await onTrigger(t, hap.value, onended); n = toMidi(n); // e.g. c3 => 48
if (soundHandle) {
sourceNode = soundHandle.node;
soundHandle.stop(t + hapDuration);
} }
// get frequency
if (!freq && typeof n === 'number') {
freq = fromMidi(n); // + 48);
}
// make oscillator
const o = getOscillator({ t, s, freq, duration: hapDuration, release });
chain.push(o);
// level down oscillators as they are really loud compared to samples i've tested
chain.push(gainNode(0.3));
// TODO: make adsr work with samples without pops
// envelope
const adsr = getADSR(attack, decay, sustain, release, 1, t, t + hapDuration);
chain.push(adsr);
} else { } else {
throw new Error(`sound ${s} not found! Is it loaded?`); // destructure adsr here, because the default should be different for synths and samples
} const { attack = 0.001, decay = 0.001, sustain = 1, release = 0.001 } = hap.value;
if (!sourceNode) { // load sample
// if onTrigger does not return anything, we will just silently skip if (speed === 0) {
// this can be used for things like speed(0) in the sampler // no playback
return; return;
} }
if (!s) {
console.warn('no sample specified');
return;
}
const soundfont = getSoundfontKey(s);
let bufferSource;
if (soundfont) {
// is soundfont
bufferSource = await globalThis.getFontBufferSource(soundfont, note || n, ac, freq);
} else {
// is sample from loaded samples(..)
bufferSource = await getSampleBufferSource(s, n, note, speed, freq);
}
// asny stuff above took too long?
if (ac.currentTime > t) { if (ac.currentTime > t) {
logger('[webaudio] skip hap: still loading', ac.currentTime - t); logger(`[sampler] still loading sound "${s}:${n}"`, 'highlight');
// console.warn('sample still loading:', s, n);
return; return;
} }
const chain = []; // audio nodes that will be connected to each other sequentially if (!bufferSource) {
chain.push(sourceNode); console.warn('no buffer source');
return;
}
bufferSource.playbackRate.value = Math.abs(speed) * bufferSource.playbackRate.value;
if (unit === 'c') {
// are there other units?
bufferSource.playbackRate.value = bufferSource.playbackRate.value * bufferSource.buffer.duration * cps;
}
let duration = soundfont || clip ? hapDuration : bufferSource.buffer.duration / bufferSource.playbackRate.value;
// "The computation of the offset into the sound is performed using the sound buffer's natural sample rate,
// rather than the current playback rate, so even if the sound is playing at twice its normal speed,
// the midway point through a 10-second audio buffer is still 5."
const offset = begin * duration * bufferSource.playbackRate.value;
duration = (end - begin) * duration;
if (loop) {
bufferSource.loop = true;
bufferSource.loopStart = offset;
bufferSource.loopEnd = offset + duration;
duration = loop * duration;
}
t += nudge;
bufferSource.start(t, offset);
if (cut !== undefined) {
cutGroups[cut]?.stop(t); // fade out?
cutGroups[cut] = bufferSource;
}
chain.push(bufferSource);
bufferSource.stop(t + duration + release);
const adsr = getADSR(attack, decay, sustain, release, 1, t, t + duration);
chain.push(adsr);
}
// gain stage // gain stage
chain.push(gainNode(gain)); chain.push(gainNode(gain));
@@ -237,9 +357,8 @@ export const webaudioOutput = async (hap, deadline, hapDuration, cps) => {
// connect chain elements together // connect chain elements together
chain.slice(1).reduce((last, current) => last.connect(current), chain[0]); chain.slice(1).reduce((last, current) => last.connect(current), chain[0]);
// toDisconnect = all the node that should be disconnected in onended callback // disconnect all nodes when source node has ended:
// this is crucial for performance chain[0].onended = () => chain.concat([delaySend, reverbSend]).forEach((n) => n?.disconnect());
toDisconnect = chain.concat([delaySend, reverbSend]);
}; };
export const webaudioOutputTrigger = (t, hap, ct, cps) => webaudioOutput(hap, t - ct, hap.duration / cps, cps); export const webaudioOutputTrigger = (t, hap, ct, cps) => webaudioOutput(hap, t - ct, hap.duration / cps, cps);
@@ -248,16 +367,3 @@ Pattern.prototype.webaudio = function () {
// TODO: refactor (t, hap, ct, cps) to (hap, deadline, duration) ? // TODO: refactor (t, hap, ct, cps) to (hap, deadline, duration) ?
return this.onTrigger(webaudioOutputTrigger); return this.onTrigger(webaudioOutputTrigger);
}; };
export function webaudioScheduler(options = {}) {
options = {
getTime: () => getAudioContext().currentTime,
defaultOutput: webaudioOutput,
...options,
};
const { defaultOutput, getTime } = options;
return new strudel.Cyclist({
...options,
onTrigger: strudel.getTrigger({ defaultOutput, getTime }),
});
}
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel.cycles/webdirt", "name": "@strudel.cycles/webdirt",
"version": "0.8.0", "version": "0.6.0",
"description": "WebDirt integration for Strudel", "description": "WebDirt integration for Strudel",
"main": "index.mjs", "main": "index.mjs",
"type": "module", "type": "module",
@@ -35,7 +35,7 @@
"WebDirt": "github:dktr0/WebDirt" "WebDirt": "github:dktr0/WebDirt"
}, },
"devDependencies": { "devDependencies": {
"vite": "^4.3.3", "vite": "^3.2.2",
"vitest": "^0.28.0" "vitest": "^0.25.7"
} }
} }
+3 -4
View File
@@ -1,7 +1,7 @@
import * as strudel from '@strudel.cycles/core'; import * as strudel from '@strudel.cycles/core';
const { Pattern } = strudel; const { Pattern } = strudel;
import * as WebDirt from 'WebDirt'; import * as WebDirt from 'WebDirt';
//import { loadBuffer, getLoadedBuffer } from '@strudel.cycles/webaudio'; import { getLoadedSamples, loadBuffer, getLoadedBuffer } from '@strudel.cycles/webaudio';
let webDirt; let webDirt;
@@ -62,9 +62,8 @@ export function loadWebDirt(config) {
* @noAutocomplete * @noAutocomplete
*/ */
Pattern.prototype.webdirt = function () { Pattern.prototype.webdirt = function () {
throw new Error('webdirt support has been dropped..');
// create a WebDirt object and initialize Web Audio context // create a WebDirt object and initialize Web Audio context
/* return this.onTrigger(async (time, e, currentTime) => { return this.onTrigger(async (time, e, currentTime) => {
if (!webDirt) { if (!webDirt) {
throw new Error('WebDirt not initialized!'); throw new Error('WebDirt not initialized!');
} }
@@ -93,5 +92,5 @@ Pattern.prototype.webdirt = function () {
webDirt.playSample(msg, deadline); webDirt.playSample(msg, deadline);
} }
} }
}); */ });
}; };
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel.cycles/xen", "name": "@strudel.cycles/xen",
"version": "0.8.0", "version": "0.6.0",
"description": "Xenharmonic API for strudel", "description": "Xenharmonic API for strudel",
"main": "index.mjs", "main": "index.mjs",
"publishConfig": { "publishConfig": {
@@ -33,7 +33,7 @@
"@strudel.cycles/core": "workspace:*" "@strudel.cycles/core": "workspace:*"
}, },
"devDependencies": { "devDependencies": {
"vite": "^4.3.3", "vite": "^3.2.2",
"vitest": "^0.28.0" "vitest": "^0.25.7"
} }
} }
+5029 -5195
View File
File diff suppressed because it is too large Load Diff
-2
View File
@@ -3,6 +3,4 @@ packages:
- "packages/*" - "packages/*"
- "website/" - "website/"
- "packages/core/examples/vite-vanilla-repl" - "packages/core/examples/vite-vanilla-repl"
- "packages/core/examples/vite-vanilla-repl-cm6"
- "packages/react/examples/nano-repl" - "packages/react/examples/nano-repl"
- "packages/web/examples/repl-example"
-3
View File
@@ -1,3 +0,0 @@
# Generated by Cargo
# will have compiled files and executables
/target/
-3445
View File
File diff suppressed because it is too large Load Diff
-32
View File
@@ -1,32 +0,0 @@
[package]
name = "app"
version = "0.1.0"
description = "A Tauri App"
authors = ["you"]
license = ""
repository = ""
default-run = "app"
edition = "2021"
rust-version = "1.60"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies]
tauri-build = { version = "1.4.0", features = [] }
[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "1.4.0", features = ["fs-all"] }
[features]
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.
# If you use cargo directly instead of tauri's cli you can use this feature flag to switch between tauri's `dev` and `build` modes.
# DO NOT REMOVE!!
custom-protocol = ["tauri/custom-protocol"]
[profile.release]
panic = "abort"
codegen-units = 1
lto = true
opt-level = "s"
-29
View File
@@ -1,29 +0,0 @@
# @strudel.cycles/tauri
Rust source files for building native desktop apps using Tauri
## Usage
Install [Rust](https://rustup.rs/) on your system.
From the project root:
- install Strudel dependencies
```js
pnpm i
```
- to run Strudel for development
```js
pnpm tauri dev
```
- to build the binary and installer/bundle
```js
pnpm tauri build
```
The binary and installer can be found in the 'src-tauri/target/release/bundle' directory
-3
View File
@@ -1,3 +0,0 @@
fn main() {
tauri_build::build()
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

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