Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 44064fea55 | |||
| 1a56d8a923 | |||
| a5e9cc77a5 | |||
| 9ed9ba33c5 | |||
| d046e01684 | |||
| b217471d2a | |||
| e4c249e568 | |||
| b4ef80fcae | |||
| 703e516458 |
@@ -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
|
||||
@@ -39,8 +39,4 @@ server/samples/old
|
||||
repl/stats.html
|
||||
coverage
|
||||
public/icons/apple-splash-*
|
||||
dev-dist
|
||||
Dirt-Samples
|
||||
tidal-drum-machines
|
||||
webaudiofontdata
|
||||
src-tauri/target
|
||||
dev-dist
|
||||
@@ -31,7 +31,7 @@ Use one of the Communication Channels listed above.
|
||||
|
||||
## Improve the Docs
|
||||
|
||||
If you find some weak spots in the [docs](https://strudel.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.
|
||||
|
||||
## Propose a Feature
|
||||
@@ -123,14 +123,7 @@ To publish all packages that have been changed since the last release, run:
|
||||
|
||||
```sh
|
||||
npm login
|
||||
|
||||
# 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
|
||||
npx lerna publish
|
||||
```
|
||||
|
||||
To manually publish a single package, increase the version in the `package.json`, then run `pnpm publish`.
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"build": "npm run prebuild && cd website && npm run build",
|
||||
"preview": "cd website && npm run preview",
|
||||
"osc": "cd packages/osc && npm run server",
|
||||
"deploy": "NODE_DEBUG=gh-pages gh-pages -d out",
|
||||
"jsdoc": "jsdoc packages/ -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",
|
||||
@@ -52,21 +53,22 @@
|
||||
"@strudel.cycles/webaudio": "workspace:*",
|
||||
"@strudel.cycles/xen": "workspace:*",
|
||||
"acorn": "^8.8.1",
|
||||
"dependency-tree": "^9.0.0"
|
||||
"dependency-tree": "^9.0.0",
|
||||
"vitest": "^0.25.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^1.4.0",
|
||||
"@vitest/ui": "^0.28.0",
|
||||
"canvas": "^2.11.2",
|
||||
"eslint": "^8.39.0",
|
||||
"@vitest/ui": "^0.25.7",
|
||||
"c8": "^7.12.0",
|
||||
"canvas": "^2.11.0",
|
||||
"eslint": "^8.28.0",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"events": "^3.3.0",
|
||||
"jsdoc": "^4.0.2",
|
||||
"gh-pages": "^4.0.0",
|
||||
"jsdoc": "^3.6.10",
|
||||
"jsdoc-json": "^2.0.2",
|
||||
"jsdoc-to-markdown": "^8.0.0",
|
||||
"lerna": "^6.6.1",
|
||||
"prettier": "^2.8.8",
|
||||
"rollup-plugin-visualizer": "^5.8.1",
|
||||
"vitest": "^0.28.0"
|
||||
"jsdoc-to-markdown": "^7.1.1",
|
||||
"lerna": "^4.0.0",
|
||||
"prettier": "^2.8.1",
|
||||
"rollup-plugin-visualizer": "^5.8.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -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 && tr.newDoc.length > 0) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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)];
|
||||
@@ -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,5 +1,4 @@
|
||||
import { 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..
|
||||
import { controls, Pattern, getDrawContext, silence, register, pure } from './index.mjs';
|
||||
const { createParams } = controls;
|
||||
|
||||
let clearColor = '#22222210';
|
||||
|
||||
@@ -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/>.
|
||||
*/
|
||||
|
||||
import { Pattern, register, sequence } from './pattern.mjs';
|
||||
import { Pattern, sequence } from './pattern.mjs';
|
||||
import { zipWith } from './util.mjs';
|
||||
|
||||
const controls = {};
|
||||
@@ -23,15 +23,6 @@ const generic_params = [
|
||||
*
|
||||
*/
|
||||
[['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.
|
||||
* Numbers too high will wrap around.
|
||||
@@ -222,7 +213,6 @@ const generic_params = [
|
||||
*
|
||||
* @name legato
|
||||
* @param {number | Pattern} duration between 0 and 1, where 1 is the length of the whole hap time
|
||||
* @noAutocomplete
|
||||
* @example
|
||||
* "c4 eb4 g4 bb4".legato("<0.125 .25 .5 .75 1 2 4>")
|
||||
*
|
||||
@@ -750,14 +740,13 @@ const generic_params = [
|
||||
['val'],
|
||||
['cps'],
|
||||
/**
|
||||
* Multiplies the duration with the given number. Also cuts samples off at the end if they exceed the duration.
|
||||
* In tidal, this would be done with legato, [which has a complicated history in strudel](https://github.com/tidalcycles/strudel/issues/111).
|
||||
* For now, if you're coming from tidal, just think clip = legato.
|
||||
* If set to 1, samples will be cut to the duration of their event.
|
||||
* In tidal, this would be done with legato, which [is about to land in strudel too](https://github.com/tidalcycles/strudel/issues/111)
|
||||
*
|
||||
* @name clip
|
||||
* @param {number | Pattern} factor >= 0
|
||||
* @param {number | Pattern} active 1 or 0
|
||||
* @example
|
||||
* note("c a f e").s("piano").clip("<.5 1 2>")
|
||||
* note("c a f e ~").s("piano").clip(1)
|
||||
*
|
||||
*/
|
||||
['clip'],
|
||||
@@ -812,15 +801,4 @@ generic_params.forEach(([names, ...aliases]) => {
|
||||
controls.createParams = (...names) =>
|
||||
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;
|
||||
|
||||
@@ -74,7 +74,6 @@ export class Cyclist {
|
||||
stop() {
|
||||
logger('[cyclist] stop');
|
||||
this.clock.stop();
|
||||
this.lastEnd = 0;
|
||||
this.setStarted(false);
|
||||
}
|
||||
setPattern(pat, autostart = false) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
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/>.
|
||||
*/
|
||||
|
||||
@@ -13,16 +13,8 @@ export const getDrawContext = (id = 'test-canvas') => {
|
||||
canvas.id = id;
|
||||
canvas.width = window.innerWidth;
|
||||
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);
|
||||
let timeout;
|
||||
window.addEventListener('resize', () => {
|
||||
timeout && clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
}, 200);
|
||||
});
|
||||
}
|
||||
return canvas.getContext('2d');
|
||||
};
|
||||
@@ -73,97 +65,3 @@ Pattern.prototype.onPaint = function (onPaint) {
|
||||
this.context = { onPaint };
|
||||
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 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"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^4.3.3"
|
||||
"vite": "^3.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@strudel.cycles/core": "workspace:*",
|
||||
|
||||
@@ -32,11 +32,7 @@ export class Hap {
|
||||
}
|
||||
|
||||
get duration() {
|
||||
return this.whole.end.sub(this.whole.begin).mul(typeof this.value?.clip === 'number' ? this.value?.clip : 1);
|
||||
}
|
||||
|
||||
get endClipped() {
|
||||
return this.whole.begin.add(this.duration);
|
||||
return this.whole.end.sub(this.whole.begin);
|
||||
}
|
||||
|
||||
wholeOrPart() {
|
||||
|
||||
@@ -18,13 +18,11 @@ export * from './util.mjs';
|
||||
export * from './speak.mjs';
|
||||
export * from './evaluate.mjs';
|
||||
export * from './repl.mjs';
|
||||
export * from './cyclist.mjs';
|
||||
export * from './logger.mjs';
|
||||
export * from './time.mjs';
|
||||
export * from './draw.mjs';
|
||||
export * from './animate.mjs';
|
||||
export * from './pianoroll.mjs';
|
||||
export * from './spiral.mjs';
|
||||
export * from './ui.mjs';
|
||||
export { default as drawLine } from './drawLine.mjs';
|
||||
export { default as gist } from './gist.js';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/core",
|
||||
"version": "0.8.2",
|
||||
"version": "0.6.8",
|
||||
"description": "Port of Tidal Cycles to JavaScript",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
@@ -35,7 +35,7 @@
|
||||
},
|
||||
"gitHead": "0e26d4e741500f5bae35b023608f062a794905c2",
|
||||
"devDependencies": {
|
||||
"vite": "^4.3.3",
|
||||
"vitest": "^0.28.0"
|
||||
"vite": "^3.2.2",
|
||||
"vitest": "^0.25.7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1127,8 +1127,6 @@ function _composeOp(a, b, func) {
|
||||
export const polyrhythm = stack;
|
||||
export const pr = stack;
|
||||
|
||||
export const pm = polymeter;
|
||||
|
||||
// methods that create patterns, which are added to patternified Pattern methods
|
||||
// TODO: remove? this is only used in old transpiler (shapeshifter)
|
||||
Pattern.prototype.factories = {
|
||||
@@ -1376,6 +1374,11 @@ export function polymeter(...args) {
|
||||
return polymeterSteps(0, ...args);
|
||||
}
|
||||
|
||||
// alias
|
||||
export function pm(...args) {
|
||||
polymeter(...args);
|
||||
}
|
||||
|
||||
export const mask = curry((a, b) => reify(b).mask(a));
|
||||
export const struct = curry((a, b) => reify(b).struct(a));
|
||||
export const superimpose = curry((a, b) => reify(b).superimpose(...a));
|
||||
@@ -1582,24 +1585,6 @@ export const range2 = register('range2', function (min, max, pat) {
|
||||
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
|
||||
|
||||
@@ -1695,9 +1680,6 @@ export const ply = register('ply', function (factor, pat) {
|
||||
* s("<bd sd> hh").fast(2) // s("[<bd sd> hh]*2")
|
||||
*/
|
||||
export const { fast, density } = register(['fast', 'density'], function (factor, pat) {
|
||||
if (factor === 0) {
|
||||
return silence;
|
||||
}
|
||||
factor = Fraction(factor);
|
||||
const fastQuery = pat.withQueryTime((t) => t.mul(factor));
|
||||
return fastQuery.withHapTime((t) => t.div(factor));
|
||||
@@ -1724,9 +1706,6 @@ export const hurry = register('hurry', function (r, pat) {
|
||||
* s("<bd sd> hh").slow(2) // s("[<bd sd> hh]/2")
|
||||
*/
|
||||
export const { slow, sparsity } = register(['slow', 'sparsity'], function (factor, pat) {
|
||||
if (factor === 0) {
|
||||
return silence;
|
||||
}
|
||||
return pat._fast(Fraction(1).div(factor));
|
||||
});
|
||||
|
||||
@@ -2053,7 +2032,7 @@ export const jux = register('jux', function (func, pat) {
|
||||
* @example
|
||||
* "<0 [2 4]>"
|
||||
* .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(
|
||||
['echoWith', 'echowith', 'stutWith', 'stutwith'],
|
||||
@@ -2203,7 +2182,6 @@ export const velocity = register('velocity', function (velocity, pat) {
|
||||
/**
|
||||
*
|
||||
* 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
|
||||
* @memberof Pattern
|
||||
* @example
|
||||
@@ -2276,7 +2254,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)
|
||||
*/
|
||||
|
||||
export const slice = register(
|
||||
const slice = register(
|
||||
'slice',
|
||||
function (npat, ipat, opat) {
|
||||
return npat.innerBind((n) =>
|
||||
@@ -2304,7 +2282,7 @@ export const slice = register(
|
||||
* 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',
|
||||
function (npat, ipat, opat) {
|
||||
const sliced = slice(npat, ipat, opat);
|
||||
@@ -2321,10 +2299,7 @@ export const splice = register(
|
||||
false, // turns off auto-patternification
|
||||
);
|
||||
|
||||
// this function will be redefined in repl.mjs to use the correct cps value.
|
||||
// It is still here to work in cases where repl.mjs is not used
|
||||
|
||||
export const { loopAt, loopat } = register(['loopAt', 'loopat'], function (factor, pat) {
|
||||
const { loopAt, loopat } = register(['loopAt', 'loopat'], function (factor, pat) {
|
||||
return _loopAt(factor, pat, 1);
|
||||
});
|
||||
|
||||
@@ -2341,6 +2316,6 @@ export const { loopAt, loopat } = register(['loopAt', 'loopat'], function (facto
|
||||
* s("rhodes").loopAtCps(4,1.5).cps(1.5)
|
||||
*/
|
||||
// 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);
|
||||
});
|
||||
|
||||
@@ -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/>.
|
||||
*/
|
||||
|
||||
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 getValue = (e) => {
|
||||
@@ -18,7 +18,7 @@ const getValue = (e) => {
|
||||
}
|
||||
note = note ?? n;
|
||||
if (typeof note === 'string') {
|
||||
return noteToMidi(note);
|
||||
return toMidi(note);
|
||||
}
|
||||
if (typeof note === 'number') {
|
||||
return note;
|
||||
@@ -50,7 +50,6 @@ Pattern.prototype.pianoroll = function ({
|
||||
timeframe: timeframeProp,
|
||||
fold = 0,
|
||||
vertical = 0,
|
||||
labels = 0,
|
||||
} = {}) {
|
||||
const ctx = getDrawContext();
|
||||
const w = ctx.canvas.width;
|
||||
@@ -83,12 +82,12 @@ Pattern.prototype.pianoroll = function ({
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
}
|
||||
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) => {
|
||||
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.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);
|
||||
let durationPx = scale(event.duration / timeExtent, 0, timeAxis);
|
||||
const value = getValue(event);
|
||||
@@ -115,14 +114,6 @@ Pattern.prototype.pianoroll = function ({
|
||||
];
|
||||
}
|
||||
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!
|
||||
const playheadPosition = scale(-from / timeExtent, ...timeRange);
|
||||
@@ -190,7 +181,6 @@ export function pianoroll({
|
||||
timeframe: timeframeProp,
|
||||
fold = 0,
|
||||
vertical = 0,
|
||||
labels = false,
|
||||
ctx,
|
||||
} = {}) {
|
||||
const w = ctx.canvas.width;
|
||||
@@ -250,7 +240,7 @@ export function pianoroll({
|
||||
const color = event.value?.color || event.context?.color;
|
||||
ctx.fillStyle = color || inactive;
|
||||
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);
|
||||
let durationPx = scale(event.duration / timeExtent, 0, timeAxis);
|
||||
const value = getValue(event);
|
||||
@@ -277,14 +267,6 @@ export function pianoroll({
|
||||
];
|
||||
}
|
||||
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!
|
||||
const playheadPosition = scale(-from / timeExtent, ...timeRange);
|
||||
@@ -302,7 +284,7 @@ export function pianoroll({
|
||||
return this;
|
||||
}
|
||||
|
||||
export function getDrawOptions(drawTime, options = {}) {
|
||||
function getOptions(drawTime, options = {}) {
|
||||
let [lookbehind, lookahead] = drawTime;
|
||||
lookbehind = Math.abs(lookbehind);
|
||||
const cycles = lookahead + lookbehind;
|
||||
@@ -311,18 +293,5 @@ export function getDrawOptions(drawTime, options = {}) {
|
||||
}
|
||||
|
||||
Pattern.prototype.punchcard = function (options) {
|
||||
return this.onPaint((ctx, time, haps, drawTime) =>
|
||||
pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, options) }),
|
||||
);
|
||||
return this.onPaint((ctx, time, haps, drawTime) => pianoroll({ ctx, time, haps, ...getOptions(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 });
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { evaluate as _evaluate } from './evaluate.mjs';
|
||||
import { logger } from './logger.mjs';
|
||||
import { setTime } from './time.mjs';
|
||||
import { evalScope } from './evaluate.mjs';
|
||||
import { register } from './pattern.mjs';
|
||||
|
||||
export function repl({
|
||||
interval,
|
||||
@@ -19,26 +18,46 @@ export function repl({
|
||||
}) {
|
||||
const scheduler = new Cyclist({
|
||||
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,
|
||||
getTime,
|
||||
onToggle,
|
||||
});
|
||||
const setPattern = (pattern, autostart = true) => {
|
||||
pattern = editPattern?.(pattern) || pattern;
|
||||
scheduler.setPattern(pattern, autostart);
|
||||
};
|
||||
setTime(() => scheduler.now()); // TODO: refactor?
|
||||
const evaluate = async (code, autostart = true) => {
|
||||
if (!code) {
|
||||
throw new Error('no code to evaluate');
|
||||
}
|
||||
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);
|
||||
|
||||
logger(`[eval] code updated`);
|
||||
setPattern(pattern, autostart);
|
||||
pattern = editPattern?.(pattern) || pattern;
|
||||
scheduler.setPattern(pattern, autostart);
|
||||
afterEval?.({ code, pattern });
|
||||
return pattern;
|
||||
} catch (err) {
|
||||
@@ -51,47 +70,9 @@ export function repl({
|
||||
const start = () => scheduler.start();
|
||||
const pause = () => scheduler.pause();
|
||||
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({
|
||||
loopAt,
|
||||
fit,
|
||||
setCps,
|
||||
setcps: setCps,
|
||||
setCpm,
|
||||
setcpm: setCpm,
|
||||
});
|
||||
|
||||
return { scheduler, evaluate, start, stop, pause, setCps, setPattern };
|
||||
return { scheduler, evaluate, start, stop, pause, setCps };
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -27,7 +27,7 @@ export const isaw2 = isaw.toBipolar();
|
||||
*
|
||||
* @return {Pattern}
|
||||
* @example
|
||||
* "c3 [eb3,g3] g2 [g3,bb3]".note().clip(saw.slow(4))
|
||||
* "c3 [eb3,g3] g2 [g3,bb3]".legato(saw.slow(4)).note()
|
||||
* @example
|
||||
* saw.range(0,8).segment(8).scale('C major').slow(4).note()
|
||||
*
|
||||
|
||||
@@ -1,118 +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 }) {
|
||||
const [w, h] = [ctx.canvas.width, ctx.canvas.height];
|
||||
ctx.clearRect(0, 0, w * 2, h * 2);
|
||||
const [cx, cy] = [w / 2, h / 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 }));
|
||||
};
|
||||
@@ -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);
|
||||
});
|
||||
@@ -8,8 +8,8 @@ import { pure } from '../pattern.mjs';
|
||||
import {
|
||||
isNote,
|
||||
tokenizeNote,
|
||||
noteToMidi,
|
||||
midiToFreq,
|
||||
toMidi,
|
||||
fromMidi,
|
||||
freqToMidi,
|
||||
_mod,
|
||||
compose,
|
||||
@@ -75,27 +75,27 @@ describe('isNote', () => {
|
||||
expect(tokenizeNote(123)).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
describe('noteToMidi', () => {
|
||||
describe('toMidi', () => {
|
||||
it('should turn notes into midi', () => {
|
||||
expect(noteToMidi('A4')).toEqual(69);
|
||||
expect(noteToMidi('C4')).toEqual(60);
|
||||
expect(noteToMidi('Db4')).toEqual(61);
|
||||
expect(noteToMidi('C3')).toEqual(48);
|
||||
expect(noteToMidi('Cb3')).toEqual(47);
|
||||
expect(noteToMidi('Cbb3')).toEqual(46);
|
||||
expect(noteToMidi('C#3')).toEqual(49);
|
||||
expect(noteToMidi('C#3')).toEqual(49);
|
||||
expect(noteToMidi('C##3')).toEqual(50);
|
||||
expect(toMidi('A4')).toEqual(69);
|
||||
expect(toMidi('C4')).toEqual(60);
|
||||
expect(toMidi('Db4')).toEqual(61);
|
||||
expect(toMidi('C3')).toEqual(48);
|
||||
expect(toMidi('Cb3')).toEqual(47);
|
||||
expect(toMidi('Cbb3')).toEqual(46);
|
||||
expect(toMidi('C#3')).toEqual(49);
|
||||
expect(toMidi('C#3')).toEqual(49);
|
||||
expect(toMidi('C##3')).toEqual(50);
|
||||
});
|
||||
it('should throw an error when given a non-note', () => {
|
||||
expect(() => noteToMidi('Q')).toThrowError(`not a note: "Q"`);
|
||||
expect(() => noteToMidi('Z')).toThrowError(`not a note: "Z"`);
|
||||
expect(() => toMidi('Q')).toThrowError(`not a note: "Q"`);
|
||||
expect(() => toMidi('Z')).toThrowError(`not a note: "Z"`);
|
||||
});
|
||||
});
|
||||
describe('midiToFreq', () => {
|
||||
describe('fromMidi', () => {
|
||||
it('should turn midi into frequency', () => {
|
||||
expect(midiToFreq(69)).toEqual(440);
|
||||
expect(midiToFreq(57)).toEqual(220);
|
||||
expect(fromMidi(69)).toEqual(440);
|
||||
expect(fromMidi(57)).toEqual(220);
|
||||
});
|
||||
});
|
||||
describe('freqToMidi', () => {
|
||||
|
||||
@@ -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
|
||||
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) => {
|
||||
if (typeof note !== 'string') {
|
||||
return [];
|
||||
}
|
||||
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)([0-9])?$/)?.slice(1) || [];
|
||||
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bs]*)([0-9])?$/)?.slice(1) || [];
|
||||
if (!pc) {
|
||||
return [];
|
||||
}
|
||||
@@ -19,16 +19,16 @@ export const tokenizeNote = (note) => {
|
||||
};
|
||||
|
||||
// turns the given note into its midi number representation
|
||||
export const noteToMidi = (note) => {
|
||||
export const toMidi = (note) => {
|
||||
const [pc, acc, oct = 3] = tokenizeNote(note);
|
||||
if (!pc) {
|
||||
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 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;
|
||||
};
|
||||
export const midiToFreq = (n) => {
|
||||
export const fromMidi = (n) => {
|
||||
return Math.pow(2, (n - 69) / 12) * 440;
|
||||
};
|
||||
|
||||
@@ -45,7 +45,7 @@ export const valueToMidi = (value, fallbackValue) => {
|
||||
return freqToMidi(freq);
|
||||
}
|
||||
if (typeof note === 'string') {
|
||||
return noteToMidi(note);
|
||||
return toMidi(note);
|
||||
}
|
||||
if (typeof note === 'number') {
|
||||
return note;
|
||||
@@ -62,19 +62,18 @@ export const valueToMidi = (value, fallbackValue) => {
|
||||
*/
|
||||
export const getFreq = (noteOrMidi) => {
|
||||
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
|
||||
*/
|
||||
export const midi2note = (n) => {
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -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 (typeof note === 'number' && context.type !== 'frequency') {
|
||||
note = midiToFreq(hap.value);
|
||||
note = fromMidi(hap.value);
|
||||
} else if (typeof note === 'number' && context.type === 'frequency') {
|
||||
note = hap.value; // legacy workaround.. will be removed in the future
|
||||
} else if (typeof note !== 'string' || !isNote(note)) {
|
||||
@@ -111,9 +110,9 @@ export const getFrequency = (hap) => {
|
||||
return getFreq(value.note || value.n || value.value);
|
||||
}
|
||||
if (typeof value === 'number' && context.type !== 'frequency') {
|
||||
value = midiToFreq(hap.value);
|
||||
value = fromMidi(hap.value);
|
||||
} else if (typeof value === 'string' && isNote(value)) {
|
||||
value = midiToFreq(noteToMidi(hap.value));
|
||||
value = fromMidi(toMidi(hap.value));
|
||||
} else if (typeof value !== 'number') {
|
||||
throw new Error('not a note or frequency: ' + value);
|
||||
}
|
||||
@@ -171,7 +170,7 @@ export function parseNumeral(numOrString) {
|
||||
return asNumber;
|
||||
}
|
||||
if (isNote(numOrString)) {
|
||||
return noteToMidi(numOrString);
|
||||
return toMidi(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 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;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/csound",
|
||||
"version": "0.8.0",
|
||||
"version": "0.6.2",
|
||||
"description": "csound bindings for strudel",
|
||||
"main": "index.mjs",
|
||||
"publishConfig": {
|
||||
@@ -32,11 +32,11 @@
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@csound/browser": "6.18.7",
|
||||
"@csound/browser": "6.18.5",
|
||||
"@strudel.cycles/core": "workspace:*",
|
||||
"@strudel.cycles/webaudio": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^4.3.3"
|
||||
"vite": "^3.2.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/eval",
|
||||
"version": "0.8.0",
|
||||
"version": "0.6.2",
|
||||
"description": "Code evaluator for strudel",
|
||||
"main": "index.mjs",
|
||||
"publishConfig": {
|
||||
@@ -44,7 +44,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@strudel.cycles/mini": "workspace:*",
|
||||
"vite": "^4.3.3",
|
||||
"vitest": "^0.28.0"
|
||||
"vite": "^3.2.2",
|
||||
"vitest": "^0.25.7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 { Pattern, isPattern, logger } from '@strudel.cycles/core';
|
||||
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:
|
||||
export const { WebMidi } = _WebMidi;
|
||||
@@ -105,7 +105,7 @@ Pattern.prototype.midi = function (output) {
|
||||
hap.ensureObjectValue();
|
||||
|
||||
// calculate time
|
||||
const timingOffset = WebMidi.time - getAudioContext().getOutputTimestamp().contextTime * 1000;
|
||||
const timingOffset = WebMidi.time - getAudioContext().currentTime * 1000;
|
||||
time = time * 1000 + timingOffset;
|
||||
|
||||
// destructure value
|
||||
@@ -114,7 +114,7 @@ Pattern.prototype.midi = function (output) {
|
||||
const duration = hap.duration.valueOf() * 1000 - 5;
|
||||
|
||||
if (note) {
|
||||
const midiNumber = noteToMidi(note);
|
||||
const midiNumber = toMidi(note);
|
||||
device.playNote(midiNumber, midichan, {
|
||||
time,
|
||||
duration,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/midi",
|
||||
"version": "0.8.0",
|
||||
"version": "0.6.0",
|
||||
"description": "Midi API for strudel",
|
||||
"main": "index.mjs",
|
||||
"publishConfig": {
|
||||
@@ -31,9 +31,9 @@
|
||||
"dependencies": {
|
||||
"@strudel.cycles/core": "workspace:*",
|
||||
"@strudel.cycles/webaudio": "workspace:*",
|
||||
"webmidi": "^3.1.5"
|
||||
"webmidi": "^3.0.21"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^4.3.3"
|
||||
"vite": "^3.2.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Generated by Peggy 3.0.2.
|
||||
// Generated by Peggy 2.0.1.
|
||||
//
|
||||
// https://peggyjs.org/
|
||||
|
||||
@@ -43,19 +43,16 @@ peg$SyntaxError.prototype.format = function(sources) {
|
||||
}
|
||||
}
|
||||
var s = this.location.start;
|
||||
var offset_s = (this.location.source && (typeof this.location.source.offset === "function"))
|
||||
? this.location.source.offset(s)
|
||||
: s;
|
||||
var loc = this.location.source + ":" + offset_s.line + ":" + offset_s.column;
|
||||
var loc = this.location.source + ":" + s.line + ":" + s.column;
|
||||
if (src) {
|
||||
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 last = s.line === e.line ? e.column : line.length + 1;
|
||||
var hatLen = (last - s.column) || 1;
|
||||
str += "\n --> " + loc + "\n"
|
||||
+ filler + " |\n"
|
||||
+ offset_s.line + " | " + line + "\n"
|
||||
+ s.line + " | " + line + "\n"
|
||||
+ filler + " | " + peg$padEnd("", s.column - 1, ' ')
|
||||
+ peg$padEnd("", hatLen, "^");
|
||||
} else {
|
||||
@@ -282,7 +279,7 @@ function peg$parse(input, options) {
|
||||
var peg$f8 = function(p, s, r) { return x => x.options_['ops'].push({ type_: "bjorklund", arguments_ :{ pulse: p, step:s, rotation:r }}) };
|
||||
var peg$f9 = function(a) { return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'slow' }}) };
|
||||
var peg$f10 = function(a) { return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'fast' }}) };
|
||||
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_: "degradeBy", arguments_ :{ amount:a } }) };
|
||||
var peg$f12 = function(s) { return x => x.options_['ops'].push({ type_: "tail", arguments_ :{ element:s } }) };
|
||||
var peg$f13 = function(s, ops) { const result = new ElementStub(s, {ops: [], weight: 1, reps: 1});
|
||||
for (const op of ops) {
|
||||
@@ -292,8 +289,8 @@ function peg$parse(input, options) {
|
||||
};
|
||||
var peg$f14 = function(s) { return new PatternStub(s, 'fastcat'); };
|
||||
var peg$f15 = function(tail) { return { alignment: 'stack', list: tail }; };
|
||||
var peg$f16 = function(tail) { return { alignment: 'rand', list: tail, seed: seed++ }; };
|
||||
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$f16 = 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); } else { return head; } };
|
||||
var peg$f18 = function(head, tail) { return new PatternStub(tail ? [head, ...tail.list] : [head], 'polymeter'); };
|
||||
var peg$f19 = function(sc) { return sc; };
|
||||
var peg$f20 = function(s) { return { name: "struct", args: { mini:s }}};
|
||||
@@ -425,11 +422,11 @@ function peg$parse(input, options) {
|
||||
}
|
||||
}
|
||||
|
||||
function peg$computeLocation(startPos, endPos, offset) {
|
||||
function peg$computeLocation(startPos, endPos) {
|
||||
var startPosDetails = peg$computePosDetails(startPos);
|
||||
var endPosDetails = peg$computePosDetails(endPos);
|
||||
|
||||
var res = {
|
||||
return {
|
||||
source: peg$source,
|
||||
start: {
|
||||
offset: startPos,
|
||||
@@ -442,11 +439,6 @@ function peg$parse(input, options) {
|
||||
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) {
|
||||
@@ -1519,20 +1511,18 @@ function peg$parse(input, options) {
|
||||
}
|
||||
|
||||
function peg$parsemini() {
|
||||
var s0, s1, s2, s3, s4, s5, s6;
|
||||
var s0, s1, s2, s3, s4;
|
||||
|
||||
s0 = peg$currPos;
|
||||
s1 = peg$parsews();
|
||||
s2 = peg$parsequote();
|
||||
if (s2 !== peg$FAILED) {
|
||||
s3 = peg$parsews();
|
||||
s4 = peg$parsestack_or_choose();
|
||||
if (s4 !== peg$FAILED) {
|
||||
s5 = peg$parsews();
|
||||
s6 = peg$parsequote();
|
||||
if (s6 !== peg$FAILED) {
|
||||
s3 = peg$parsestack_or_choose();
|
||||
if (s3 !== peg$FAILED) {
|
||||
s4 = peg$parsequote();
|
||||
if (s4 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f19(s4);
|
||||
s0 = peg$f19(s3);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -2195,13 +2185,10 @@ function peg$parse(input, options) {
|
||||
this.location_ = location();
|
||||
}
|
||||
|
||||
var PatternStub = function(source, alignment, seed)
|
||||
var PatternStub = function(source, alignment)
|
||||
{
|
||||
this.type_ = "pattern";
|
||||
this.arguments_ = { alignment: alignment };
|
||||
if (seed !== undefined) {
|
||||
this.arguments_.seed = seed;
|
||||
}
|
||||
this.arguments_ = { alignment : alignment};
|
||||
this.source_ = source;
|
||||
}
|
||||
|
||||
@@ -2227,7 +2214,6 @@ function peg$parse(input, options) {
|
||||
this.options_ = options;
|
||||
}
|
||||
|
||||
var seed = 0;
|
||||
|
||||
peg$result = peg$startRuleFunction();
|
||||
|
||||
|
||||
@@ -19,13 +19,10 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
this.location_ = location();
|
||||
}
|
||||
|
||||
var PatternStub = function(source, alignment, seed)
|
||||
var PatternStub = function(source, alignment)
|
||||
{
|
||||
this.type_ = "pattern";
|
||||
this.arguments_ = { alignment: alignment };
|
||||
if (seed !== undefined) {
|
||||
this.arguments_.seed = seed;
|
||||
}
|
||||
this.arguments_ = { alignment : alignment};
|
||||
this.source_ = source;
|
||||
}
|
||||
|
||||
@@ -51,7 +48,6 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
this.options_ = options;
|
||||
}
|
||||
|
||||
var seed = 0;
|
||||
}
|
||||
|
||||
start = statement
|
||||
@@ -141,7 +137,7 @@ op_fast = "*"a:slice
|
||||
{ return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'fast' }}) }
|
||||
|
||||
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
|
||||
{ return x => x.options_['ops'].push({ type_: "tail", arguments_ :{ element:s } }) }
|
||||
@@ -166,12 +162,12 @@ stack_tail = tail:(comma @sequence)+
|
||||
// a choose is a series of pipe-separated sequence, one of which is
|
||||
// chosen at random, each cycle
|
||||
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
|
||||
// underlying element
|
||||
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?
|
||||
{ return new PatternStub(tail ? [head, ...tail.list] : [head], 'polymeter'); }
|
||||
@@ -182,7 +178,7 @@ polymeter_stack = head:sequence tail:stack_tail?
|
||||
// Experimental haskellish parser begins
|
||||
|
||||
// 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; }
|
||||
|
||||
// ------------------ operators ---------------------------
|
||||
|
||||
@@ -7,7 +7,12 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
import * as krill from './krill-parser.js';
|
||||
import * as strudel from '@strudel.cycles/core';
|
||||
|
||||
const randOffset = 0.0003;
|
||||
/* var _seedState = 0;
|
||||
const randOffset = 0.0002;
|
||||
|
||||
function _nextSeed() {
|
||||
return _seedState++;
|
||||
} */
|
||||
|
||||
const applyOptions = (parent, code) => (pat, i) => {
|
||||
const ast = parent.source_[i];
|
||||
@@ -39,9 +44,27 @@ const applyOptions = (parent, code) => (pat, i) => {
|
||||
break;
|
||||
}
|
||||
case 'degradeBy': {
|
||||
pat = strudel
|
||||
.reify(pat)
|
||||
._degradeByWith(strudel.rand.early(randOffset * op.arguments_.seed), op.arguments_.amount ?? 0.5);
|
||||
// TODO: find out what is right here
|
||||
// example:
|
||||
/*
|
||||
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;
|
||||
}
|
||||
case 'tail': {
|
||||
@@ -91,7 +114,9 @@ export function patternifyAST(ast, code) {
|
||||
return strudel.stack(...aligned);
|
||||
}
|
||||
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);
|
||||
if (!weightedChildren && alignment === 'slowcat') {
|
||||
@@ -190,8 +215,3 @@ export function minify(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);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/mini",
|
||||
"version": "0.8.2",
|
||||
"version": "0.6.0",
|
||||
"description": "Mini notation for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
@@ -35,8 +35,8 @@
|
||||
"@strudel.cycles/core": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"peggy": "^3.0.2",
|
||||
"vite": "^4.3.3",
|
||||
"vitest": "^0.28.0"
|
||||
"peggy": "^2.0.1",
|
||||
"vite": "^3.2.2",
|
||||
"vitest": "^0.25.7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,22 +140,10 @@ describe('mini', () => {
|
||||
expect(haps.length < 230).toBe(true);
|
||||
// 'Had too many cycles remaining after degradeBy 0.8');
|
||||
});
|
||||
it('supports multiple independent uses of the random choice operator ("|")', () => {
|
||||
const numCycles = 1000;
|
||||
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 lists', () => {
|
||||
expect(minV('a:b c:d:[e:f] g')).toEqual([['a', 'b'], ['c', 'd', ['e', 'f']], 'g']);
|
||||
});
|
||||
it('supports the random choice operator ("|") with nesting', () => {
|
||||
/*it('supports the random choice operator ("|") with nesting', () => {
|
||||
const numCycles = 900;
|
||||
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.
|
||||
@@ -180,8 +168,6 @@ describe('mini', () => {
|
||||
// 15.086 is the chisq for 5 degrees of freedom at 99%, so for 99% of uniformly-distributed
|
||||
// PRNG, this test should succeed
|
||||
expect(chisq <= 15.086).toBe(true);
|
||||
});
|
||||
it('supports lists', () => {
|
||||
expect(minV('a:b c:d:[e:f] g')).toEqual([['a', 'b'], ['c', 'd', ['e', 'f']], 'g']);
|
||||
});
|
||||
// assert(chisq <= 15.086, chisq + ' was expected to be less than 15.086 under chi-squared test');
|
||||
});*/
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/osc",
|
||||
"version": "0.8.0",
|
||||
"version": "0.6.0",
|
||||
"description": "OSC messaging for strudel",
|
||||
"main": "osc.mjs",
|
||||
"publishConfig": {
|
||||
@@ -40,7 +40,7 @@
|
||||
"osc-js": "^2.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"pkg": "^5.8.1",
|
||||
"vite": "^4.3.3"
|
||||
"pkg": "^5.7.0",
|
||||
"vite": "^3.2.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,18 +15,17 @@
|
||||
"@strudel.cycles/osc": "workspace:*",
|
||||
"@strudel.cycles/mini": "workspace:*",
|
||||
"@strudel.cycles/transpiler": "workspace:*",
|
||||
"@strudel.cycles/soundfonts": "workspace:*",
|
||||
"@strudel.cycles/webaudio": "workspace:*",
|
||||
"@strudel.cycles/tonal": "workspace:*",
|
||||
"@strudel.cycles/react": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.2.1",
|
||||
"@vitejs/plugin-react": "^4.0.0",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"postcss": "^8.4.23",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"vite": "^4.3.3"
|
||||
"@types/react": "^18.0.17",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@vitejs/plugin-react": "^2.0.1",
|
||||
"autoprefixer": "^10.4.8",
|
||||
"postcss": "^8.4.16",
|
||||
"tailwindcss": "^3.1.8",
|
||||
"vite": "^3.0.7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +1,22 @@
|
||||
import { controls, evalScope } from '@strudel.cycles/core';
|
||||
import { CodeMirror, useHighlighting, useKeydown, useStrudel, flash } from '@strudel.cycles/react';
|
||||
import {
|
||||
getAudioContext,
|
||||
initAudioOnFirstClick,
|
||||
panic,
|
||||
webaudioOutput,
|
||||
registerSynthSounds,
|
||||
} from '@strudel.cycles/webaudio';
|
||||
import { registerSoundfonts } from '@strudel.cycles/soundfonts';
|
||||
import { getAudioContext, initAudioOnFirstClick, panic, webaudioOutput } from '@strudel.cycles/webaudio';
|
||||
import { useCallback, useState } from 'react';
|
||||
import './style.css';
|
||||
// import { prebake } from '../../../../../repl/src/prebake.mjs';
|
||||
|
||||
initAudioOnFirstClick();
|
||||
|
||||
async function init() {
|
||||
// TODO: only import stuff when play is pressed?
|
||||
const loadModules = evalScope(
|
||||
controls,
|
||||
import('@strudel.cycles/core'),
|
||||
import('@strudel.cycles/tonal'),
|
||||
import('@strudel.cycles/mini'),
|
||||
import('@strudel.cycles/xen'),
|
||||
import('@strudel.cycles/webaudio'),
|
||||
import('@strudel.cycles/osc'),
|
||||
);
|
||||
|
||||
await Promise.all([loadModules, registerSynthSounds(), registerSoundfonts()]);
|
||||
}
|
||||
init();
|
||||
// TODO: only import stuff when play is pressed?
|
||||
evalScope(
|
||||
controls,
|
||||
import('@strudel.cycles/core'),
|
||||
import('@strudel.cycles/tonal'),
|
||||
import('@strudel.cycles/mini'),
|
||||
import('@strudel.cycles/xen'),
|
||||
import('@strudel.cycles/webaudio'),
|
||||
import('@strudel.cycles/osc'),
|
||||
);
|
||||
|
||||
const defaultTune = `samples({
|
||||
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
|
||||
.add(perlin.range(0,.5)) // random pitch variation
|
||||
.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
|
||||
.s('sawtooth') // waveform
|
||||
.gain(.4) // turn down
|
||||
@@ -52,7 +40,7 @@ stack(
|
||||
,"<Am7!3 <Em7 E7b13 Em7 Ebm7b5>>".voicings('lefthand') // chords
|
||||
.superimpose(x=>x.add(.04)) // add second, slightly detuned voice
|
||||
.add(perlin.range(0,.5)) // random pitch variation
|
||||
.note() // wrap in "n"
|
||||
.n() // wrap in "n"
|
||||
.s('square') // waveform
|
||||
.gain(.16) // turn down
|
||||
.cutoff(500) // fixed cutoff
|
||||
@@ -61,7 +49,7 @@ stack(
|
||||
,"a4 c5 <e6 a6>".struct("x(5,8)")
|
||||
.superimpose(x=>x.add(.04)) // add second, slightly detuned voice
|
||||
.add(perlin.range(0,.5)) // random pitch variation
|
||||
.note() // wrap in "note"
|
||||
.n() // wrap in "n"
|
||||
.decay(.1).sustain(0) // make notes short
|
||||
.s('triangle') // waveform
|
||||
.degradeBy(perlin.range(0,.5)) // randomly controlled random removal :)
|
||||
@@ -115,7 +103,7 @@ function App() {
|
||||
}
|
||||
}
|
||||
},
|
||||
[scheduler, evaluate, view, code],
|
||||
[scheduler, evaluate, view],
|
||||
),
|
||||
);
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/react",
|
||||
"version": "0.8.0",
|
||||
"version": "0.6.4",
|
||||
"description": "React components for strudel",
|
||||
"main": "src/index.js",
|
||||
"publishConfig": {
|
||||
@@ -32,18 +32,18 @@
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.6.0",
|
||||
"@codemirror/lang-javascript": "^6.1.7",
|
||||
"@codemirror/autocomplete": "^6.4.0",
|
||||
"@codemirror/lang-javascript": "^6.1.1",
|
||||
"@codemirror/state": "^6.2.0",
|
||||
"@codemirror/view": "^6.10.0",
|
||||
"@lezer/highlight": "^1.1.4",
|
||||
"@replit/codemirror-emacs": "^6.0.1",
|
||||
"@replit/codemirror-vim": "^6.0.14",
|
||||
"@codemirror/view": "^6.7.3",
|
||||
"@lezer/highlight": "^1.1.3",
|
||||
"@replit/codemirror-emacs": "^6.0.0",
|
||||
"@replit/codemirror-vim": "^6.0.6",
|
||||
"@strudel.cycles/core": "workspace:*",
|
||||
"@strudel.cycles/transpiler": "workspace:*",
|
||||
"@strudel.cycles/webaudio": "workspace:*",
|
||||
"@uiw/codemirror-themes": "^4.19.16",
|
||||
"@uiw/react-codemirror": "^4.19.16",
|
||||
"@uiw/codemirror-themes": "^4.12.4",
|
||||
"@uiw/react-codemirror": "^4.12.4",
|
||||
"react-hook-inview": "^4.5.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -51,14 +51,14 @@
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.2.1",
|
||||
"@vitejs/plugin-react": "^4.0.0",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"postcss": "^8.4.23",
|
||||
"@types/react": "^18.0.28",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@vitejs/plugin-react": "^2.2.0",
|
||||
"autoprefixer": "^10.4.7",
|
||||
"postcss": "^8.4.18",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"vite": "^4.3.3"
|
||||
"tailwindcss": "^3.0.24",
|
||||
"vite": "^3.2.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,12 @@ import React, { useMemo } from 'react';
|
||||
import _CodeMirror from '@uiw/react-codemirror';
|
||||
import { EditorView, Decoration } from '@codemirror/view';
|
||||
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 './style.css';
|
||||
import { useCallback } from 'react';
|
||||
import { autocompletion } from '@codemirror/autocomplete';
|
||||
import { strudelAutocomplete } from './Autocomplete';
|
||||
//import { strudelAutocomplete } from './Autocomplete';
|
||||
import { vim } from '@replit/codemirror-vim';
|
||||
import { emacs } from '@replit/codemirror-emacs';
|
||||
|
||||
@@ -20,7 +20,7 @@ const flashField = StateField.define({
|
||||
try {
|
||||
for (let e of tr.effects) {
|
||||
if (e.is(setFlash)) {
|
||||
if (e.value && tr.newDoc?.length?.length > 0) {
|
||||
if (e.value) {
|
||||
const mark = Decoration.mark({ attributes: { style: `background-color: #FFCA2880` } });
|
||||
flash = Decoration.set([mark.range(0, tr.newDoc.length)]);
|
||||
} else {
|
||||
@@ -50,9 +50,6 @@ const highlightField = StateField.define({
|
||||
return Decoration.none;
|
||||
},
|
||||
update(highlights, tr) {
|
||||
// proper fix for https://github.com/tidalcycles/strudel/issues/178
|
||||
// see https://discuss.codemirror.net/t/uncatchable-out-of-range-errors-when-document-changes/4864/5
|
||||
highlights = highlights.map(tr.changes);
|
||||
try {
|
||||
for (let e of tr.effects) {
|
||||
if (e.is(setHighlights)) {
|
||||
@@ -61,19 +58,15 @@ const highlightField = StateField.define({
|
||||
haps
|
||||
.map((hap) =>
|
||||
(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 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
|
||||
}
|
||||
let mark;
|
||||
if (color) {
|
||||
mark = Decoration.mark({ attributes: { style: `outline: 2px solid ${color};` } });
|
||||
} else {
|
||||
mark = Decoration.mark({ attributes: { class: `outline outline-2 outline-foreground` } });
|
||||
}
|
||||
//const mark = Decoration.mark({ attributes: { style: `outline: 2px solid ${color};` } });
|
||||
const mark = Decoration.mark({ attributes: { class: `outline outline-2 outline-foreground` } });
|
||||
return mark.range(from, to);
|
||||
}),
|
||||
)
|
||||
@@ -91,7 +84,14 @@ const highlightField = StateField.define({
|
||||
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({
|
||||
value,
|
||||
@@ -100,9 +100,6 @@ export default function CodeMirror({
|
||||
onSelectionChange,
|
||||
theme,
|
||||
keybindings,
|
||||
isLineNumbersDisplayed,
|
||||
isAutoCompletionEnabled,
|
||||
isLineWrappingEnabled,
|
||||
fontSize = 18,
|
||||
fontFamily = 'monospace',
|
||||
options,
|
||||
@@ -114,14 +111,12 @@ export default function CodeMirror({
|
||||
},
|
||||
[onChange],
|
||||
);
|
||||
|
||||
const handleOnCreateEditor = useCallback(
|
||||
(view) => {
|
||||
onViewChanged?.(view);
|
||||
},
|
||||
[onViewChanged],
|
||||
);
|
||||
|
||||
const handleOnUpdate = useCallback(
|
||||
(viewUpdate) => {
|
||||
if (viewUpdate.selectionSet && onSelectionChange) {
|
||||
@@ -130,33 +125,16 @@ export default function CodeMirror({
|
||||
},
|
||||
[onSelectionChange],
|
||||
);
|
||||
|
||||
const extensions = useMemo(() => {
|
||||
let _extensions = [...staticExtensions];
|
||||
let bindings = {
|
||||
vim,
|
||||
emacs,
|
||||
};
|
||||
|
||||
if (bindings[keybindings]) {
|
||||
_extensions.push(bindings[keybindings]());
|
||||
return [...staticExtensions, bindings[keybindings]()];
|
||||
}
|
||||
|
||||
if (isAutoCompletionEnabled) {
|
||||
_extensions.push(javascriptLanguage.data.of({ autocomplete: strudelAutocomplete }));
|
||||
} else {
|
||||
_extensions.push(autocompletion({ override: [] }));
|
||||
}
|
||||
|
||||
if (isLineWrappingEnabled) {
|
||||
_extensions.push(EditorView.lineWrapping);
|
||||
}
|
||||
|
||||
return _extensions;
|
||||
}, [keybindings, isAutoCompletionEnabled, isLineWrappingEnabled]);
|
||||
|
||||
const basicSetup = useMemo(() => ({ lineNumbers: isLineNumbersDisplayed }), [isLineNumbersDisplayed]);
|
||||
|
||||
return staticExtensions;
|
||||
}, [keybindings]);
|
||||
return (
|
||||
<div style={{ fontSize, fontFamily }} className="w-full">
|
||||
<_CodeMirror
|
||||
@@ -166,7 +144,6 @@ export default function CodeMirror({
|
||||
onCreateEditor={handleOnCreateEditor}
|
||||
onUpdate={handleOnUpdate}
|
||||
extensions={extensions}
|
||||
basicSetup={basicSetup}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -18,24 +18,16 @@ export function MiniRepl({
|
||||
tune,
|
||||
hideOutsideView = false,
|
||||
enableKeyboard,
|
||||
onTrigger,
|
||||
drawTime,
|
||||
punchcard,
|
||||
punchcardLabels,
|
||||
onPaint,
|
||||
canvasHeight = 200,
|
||||
fontSize = 18,
|
||||
fontFamily,
|
||||
hideHeader = false,
|
||||
theme,
|
||||
keybindings,
|
||||
isLineNumbersDisplayed,
|
||||
}) {
|
||||
drawTime = drawTime || (punchcard ? [0, 4] : undefined);
|
||||
const evalOnMount = !!drawTime;
|
||||
const drawContext = useCallback(
|
||||
punchcard ? (canvasId) => document.querySelector('#' + canvasId)?.getContext('2d') : null,
|
||||
[punchcard],
|
||||
!!drawTime ? (canvasId) => document.querySelector('#' + canvasId)?.getContext('2d') : null,
|
||||
[drawTime],
|
||||
);
|
||||
const {
|
||||
code,
|
||||
@@ -55,18 +47,7 @@ export function MiniRepl({
|
||||
} = useStrudel({
|
||||
initialCode: tune,
|
||||
defaultOutput: webaudioOutput,
|
||||
editPattern: (pat, id) => {
|
||||
//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;
|
||||
},
|
||||
editPattern: (pat) => (punchcard ? pat.punchcard() : pat),
|
||||
getTime,
|
||||
evalOnMount,
|
||||
drawContext,
|
||||
@@ -101,7 +82,7 @@ export function MiniRepl({
|
||||
e.preventDefault();
|
||||
flash(view);
|
||||
await activateCode();
|
||||
} else if (e.key === '.' || e.code === 'Period') {
|
||||
} else if (e.code === 'Period') {
|
||||
stop();
|
||||
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([]);
|
||||
useLogger(
|
||||
useCallback((e) => {
|
||||
@@ -120,7 +121,7 @@ export function MiniRepl({
|
||||
// const logId = data?.pattern?.meta?.id;
|
||||
if (logId === replId) {
|
||||
setLog((l) => {
|
||||
return l.concat([e.detail]).slice(-8);
|
||||
return l.concat([e.detail]).slice(-10);
|
||||
});
|
||||
}
|
||||
}, []),
|
||||
@@ -128,46 +129,33 @@ export function MiniRepl({
|
||||
|
||||
return (
|
||||
<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">
|
||||
<button
|
||||
className={cx(
|
||||
'cursor-pointer w-16 flex items-center justify-center p-1 border-r border-lineHighlight text-foreground bg-lineHighlight hover:bg-background',
|
||||
started ? 'animate-pulse' : '',
|
||||
)}
|
||||
onClick={() => togglePlay()}
|
||||
>
|
||||
<Icon type={started ? 'stop' : 'play'} />
|
||||
</button>
|
||||
<button
|
||||
className={cx(
|
||||
'w-16 flex items-center justify-center p-1 text-foreground border-lineHighlight bg-lineHighlight',
|
||||
isDirty ? 'text-foreground hover:bg-background cursor-pointer' : 'opacity-50 cursor-not-allowed',
|
||||
)}
|
||||
onClick={() => activateCode()}
|
||||
>
|
||||
<Icon type="refresh" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex justify-between bg-lineHighlight">
|
||||
<div className="flex">
|
||||
<button
|
||||
className={cx(
|
||||
'cursor-pointer w-16 flex items-center justify-center p-1 border-r border-lineHighlight text-foreground bg-lineHighlight hover:bg-background',
|
||||
started ? 'animate-pulse' : '',
|
||||
)}
|
||||
onClick={() => togglePlay()}
|
||||
>
|
||||
<Icon type={started ? 'stop' : 'play'} />
|
||||
</button>
|
||||
<button
|
||||
className={cx(
|
||||
'w-16 flex items-center justify-center p-1 text-foreground border-lineHighlight bg-lineHighlight',
|
||||
isDirty ? 'text-foreground hover:bg-background cursor-pointer' : 'opacity-50 cursor-not-allowed',
|
||||
)}
|
||||
onClick={() => activateCode()}
|
||||
>
|
||||
<Icon type="refresh" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="overflow-auto relative">
|
||||
{show && (
|
||||
<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>}
|
||||
{error && <div className="text-right p-1 text-sm text-red-200">{error.message}</div>}
|
||||
</div>
|
||||
{punchcard && (
|
||||
<div className="overflow-auto relative">
|
||||
{show && <CodeMirror6 value={code} onChange={setCode} onViewChanged={setView} theme={theme} />}
|
||||
</div>
|
||||
{drawTime && (
|
||||
<canvas
|
||||
id={canvasId}
|
||||
className="w-full pointer-events-none"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
:root {
|
||||
--background: #222;
|
||||
--lineBackground: #22222299;
|
||||
--lineBackground: #22222250;
|
||||
--foreground: #fff;
|
||||
--caret: #ffcc00;
|
||||
--selection: rgba(128, 203, 196, 0.5);
|
||||
@@ -24,7 +24,3 @@
|
||||
.cm-theme-light {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
footer {
|
||||
z-index: 0 !important;
|
||||
}
|
||||
|
||||
@@ -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 span = [round(begin), round(audioTime + 1 / 60)];
|
||||
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());
|
||||
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
|
||||
|
||||
@@ -29,8 +29,7 @@ function useStrudel({
|
||||
const [pattern, setPattern] = useState();
|
||||
const [started, setStarted] = useState(false);
|
||||
const isDirty = code !== activeCode;
|
||||
//const shouldPaint = useCallback((pat) => !!(pat?.context?.onPaint && drawContext), [drawContext]);
|
||||
const shouldPaint = useCallback((pat) => !!pat?.context?.onPaint, []);
|
||||
const shouldPaint = useCallback((pat) => !!(pat?.context?.onPaint && drawContext), [drawContext]);
|
||||
|
||||
// TODO: make sure this hook reruns when scheduler.started changes
|
||||
const { scheduler, evaluate, start, stop, pause, setCps } = useMemo(
|
||||
@@ -47,9 +46,9 @@ function useStrudel({
|
||||
drawContext,
|
||||
transpiler,
|
||||
editPattern,
|
||||
beforeEval: async ({ code }) => {
|
||||
beforeEval: ({ code }) => {
|
||||
setCode(code);
|
||||
await beforeEval?.();
|
||||
beforeEval?.();
|
||||
},
|
||||
afterEval: (res) => {
|
||||
const { pattern: _pattern, code } = res;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/serial",
|
||||
"version": "0.8.0",
|
||||
"version": "0.6.0",
|
||||
"description": "Webserial API for strudel",
|
||||
"main": "serial.mjs",
|
||||
"publishConfig": {
|
||||
@@ -32,6 +32,6 @@
|
||||
"@strudel.cycles/core": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^4.3.3"
|
||||
"vite": "^3.2.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { noteToMidi, freqToMidi } from '@strudel.cycles/core';
|
||||
import { getAudioContext, registerSound, getEnvelope } from '@strudel.cycles/webaudio';
|
||||
import gm from './gm.mjs';
|
||||
import { toMidi } from '@strudel.cycles/core';
|
||||
|
||||
let loadCache = {};
|
||||
async function loadFont(name) {
|
||||
@@ -10,6 +8,7 @@ async function loadFont(name) {
|
||||
const load = async () => {
|
||||
// TODO: make soundfont source configurable
|
||||
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());
|
||||
let [_, data] = preset.split('={');
|
||||
return eval('{' + data);
|
||||
@@ -18,24 +17,15 @@ async function loadFont(name) {
|
||||
return loadCache[name];
|
||||
}
|
||||
|
||||
export async function getFontBufferSource(name, value, ac) {
|
||||
let { note = 'c3', freq } = value;
|
||||
let midi;
|
||||
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}"`);
|
||||
export async function getFontBufferSource(name, pitch, ac) {
|
||||
if (typeof pitch === 'string') {
|
||||
pitch = toMidi(pitch);
|
||||
}
|
||||
|
||||
const { buffer, zone } = await getFontPitch(name, midi, ac);
|
||||
const { buffer, zone } = await getFontPitch(name, pitch, ac);
|
||||
const src = ac.createBufferSource();
|
||||
src.buffer = buffer;
|
||||
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.playbackRate.value = playbackRate;
|
||||
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 },
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { getFontBufferSource, registerSoundfonts } from './fontloader.mjs';
|
||||
import { getFontBufferSource } from './fontloader.mjs';
|
||||
import * as soundfontList from './list.mjs';
|
||||
import { startPresetNote } from 'sfumato';
|
||||
import { loadSoundfont } from './sfumato.mjs';
|
||||
|
||||
export { loadSoundfont, startPresetNote, getFontBufferSource, soundfontList, registerSoundfonts };
|
||||
export { loadSoundfont, startPresetNote, getFontBufferSource, soundfontList };
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/soundfonts",
|
||||
"version": "0.8.2",
|
||||
"version": "0.6.0",
|
||||
"description": "Soundsfont support for strudel",
|
||||
"main": "index.mjs",
|
||||
"publishConfig": {
|
||||
@@ -36,7 +36,7 @@
|
||||
"soundfont2": "^0.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"node-fetch": "^3.3.1",
|
||||
"vite": "^4.3.3"
|
||||
"node-fetch": "^3.2.6",
|
||||
"vite": "^3.2.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Pattern, getPlayableNoteValue, noteToMidi } from '@strudel.cycles/core';
|
||||
import { getAudioContext, registerSound } from '@strudel.cycles/webaudio';
|
||||
import { Pattern, getPlayableNoteValue, toMidi } from '@strudel.cycles/core';
|
||||
import { getAudioContext } from '@strudel.cycles/webaudio';
|
||||
import { loadSoundfont as _loadSoundfont, startPresetNote } from 'sfumato';
|
||||
|
||||
Pattern.prototype.soundfont = function (sf, n = 0) {
|
||||
@@ -8,7 +8,7 @@ Pattern.prototype.soundfont = function (sf, n = 0) {
|
||||
const note = getPlayableNoteValue(h);
|
||||
const preset = sf.presets[n % sf.presets.length];
|
||||
const deadline = ctx.currentTime + t - ct;
|
||||
const args = [ctx, preset, noteToMidi(note), deadline];
|
||||
const args = [ctx, preset, toMidi(note), deadline];
|
||||
const stop = startPresetNote(...args);
|
||||
stop(deadline + h.duration);
|
||||
});
|
||||
@@ -21,29 +21,5 @@ export function loadSoundfont(url) {
|
||||
}
|
||||
const sf = _loadSoundfont(url);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/tonal",
|
||||
"version": "0.8.2",
|
||||
"version": "0.6.0",
|
||||
"description": "Tonal functions for strudel",
|
||||
"main": "index.mjs",
|
||||
"publishConfig": {
|
||||
@@ -34,10 +34,10 @@
|
||||
"@strudel.cycles/core": "workspace:*",
|
||||
"@tonaljs/tonal": "^4.7.2",
|
||||
"chord-voicings": "^0.0.1",
|
||||
"webmidi": "^3.1.5"
|
||||
"webmidi": "^3.0.21"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^4.3.3",
|
||||
"vitest": "^0.28.0"
|
||||
"vite": "^3.2.2",
|
||||
"vitest": "^0.25.7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,7 +145,7 @@ export const scaleTranspose = register('scaleTranspose', function (offset /* : n
|
||||
export const scale = register('scale', function (scale, pat) {
|
||||
// Supports ':' list syntax in mininotation
|
||||
if (Array.isArray(scale)) {
|
||||
scale = scale.flat().join(' ');
|
||||
scale = scale.join(' ');
|
||||
}
|
||||
return pat.withHap((hap) => {
|
||||
const isObject = typeof hap.value === 'object';
|
||||
|
||||
@@ -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 _voicings from 'chord-voicings';
|
||||
const { dictionaryVoicing, minTopNoteDiff } = _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'],
|
||||
};
|
||||
const { dictionaryVoicing, minTopNoteDiff, lefthand } = _voicings.default || _voicings; // parcel module resolution fuckup
|
||||
|
||||
export const voicingRegistry = {
|
||||
lefthand: { dictionary: lefthand, range: ['F3', 'A4'] },
|
||||
triads: { dictionary: triads },
|
||||
guidetones: { dictionary: guidetones },
|
||||
};
|
||||
export const setVoicingRange = (name, range) => addVoicings(name, voicingRegistry[name].dictionary, range);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/tone",
|
||||
"version": "0.8.0",
|
||||
"version": "0.6.0",
|
||||
"description": "Tone.js API for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
@@ -34,7 +34,7 @@
|
||||
"tone": "^14.7.77"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^4.3.3",
|
||||
"vitest": "^0.28.0"
|
||||
"vite": "^3.2.2",
|
||||
"vitest": "^0.25.7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"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.",
|
||||
"main": "index.mjs",
|
||||
"publishConfig": {
|
||||
@@ -36,7 +36,7 @@
|
||||
"estree-walker": "^3.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^4.3.3",
|
||||
"vitest": "^0.28.0"
|
||||
"vite": "^3.2.2",
|
||||
"vitest": "^0.25.8"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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!
|
||||
@@ -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>
|
||||
@@ -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:*"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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 './sampler.mjs';
|
||||
export * from './helpers.mjs';
|
||||
export * from './synth.mjs';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/webaudio",
|
||||
"version": "0.8.2",
|
||||
"version": "0.6.1",
|
||||
"description": "Web Audio helpers for Strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
@@ -34,10 +34,9 @@
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@strudel.cycles/core": "workspace:*",
|
||||
"nanostores": "^0.8.1"
|
||||
"@strudel.cycles/core": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^4.3.3"
|
||||
"vite": "^3.2.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { logger, noteToMidi, valueToMidi } from '@strudel.cycles/core';
|
||||
import { getAudioContext, registerSound } from './index.mjs';
|
||||
import { getEnvelope } from './helpers.mjs';
|
||||
import { logger, toMidi, valueToMidi } from '@strudel.cycles/core';
|
||||
import { getAudioContext } from './index.mjs';
|
||||
|
||||
const bufferCache = {}; // string: Promise<ArrayBuffer>
|
||||
const loadCache = {}; // string: Promise<ArrayBuffer>
|
||||
@@ -21,7 +20,7 @@ function humanFileSize(bytes, si) {
|
||||
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;
|
||||
if (freq !== undefined && note !== undefined) {
|
||||
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
|
||||
|
||||
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;
|
||||
if (Array.isArray(bank)) {
|
||||
sampleUrl = bank[n % bank.length];
|
||||
} else {
|
||||
const midiDiff = (noteA) => noteToMidi(noteA) - midi;
|
||||
const midiDiff = (noteA) => toMidi(noteA) - midi;
|
||||
// object format will expect keys as notes
|
||||
const closest = Object.keys(bank)
|
||||
.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
|
||||
sampleUrl = bank[closest][n % bank[closest].length];
|
||||
}
|
||||
if (resolveUrl) {
|
||||
sampleUrl = await resolveUrl(sampleUrl);
|
||||
}
|
||||
let buffer = await loadBuffer(sampleUrl, ac, s, n);
|
||||
if (speed < 0) {
|
||||
// should this be cached?
|
||||
@@ -94,45 +107,7 @@ export const getLoadedBuffer = (url) => {
|
||||
return bufferCache[url];
|
||||
};
|
||||
|
||||
export const processSampleMap = (sampleMap, fn, baseUrl = sampleMap._base || '') => {
|
||||
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;
|
||||
}
|
||||
let sampleCache = { current: undefined };
|
||||
|
||||
/**
|
||||
* 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') {
|
||||
// check if custom prefix handler
|
||||
const handler = getSamplesPrefixHandler(sampleMap);
|
||||
if (handler) {
|
||||
return handler(sampleMap);
|
||||
}
|
||||
if (sampleMap.startsWith('github:')) {
|
||||
let [_, path] = sampleMap.split('github:');
|
||||
path = path.endsWith('/') ? path.slice(0, -1) : path;
|
||||
@@ -171,118 +141,43 @@ export const samples = async (sampleMap, baseUrl = sampleMap._base || '', option
|
||||
}
|
||||
return fetch(sampleMap)
|
||||
.then((res) => res.json())
|
||||
.then((json) => samples(json, baseUrl || json._base || base, options))
|
||||
.then((json) => samples(json, baseUrl || json._base || base))
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
throw new Error(`error loading "${sampleMap}"`);
|
||||
});
|
||||
}
|
||||
const { prebake, tag } = options;
|
||||
processSampleMap(
|
||||
sampleMap,
|
||||
(key, value) =>
|
||||
registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value), {
|
||||
type: 'sample',
|
||||
samples: value,
|
||||
baseUrl,
|
||||
prebake,
|
||||
tag,
|
||||
sampleCache.current = {
|
||||
...sampleCache.current,
|
||||
...Object.fromEntries(
|
||||
Object.entries(sampleMap).map(([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)];
|
||||
}
|
||||
// 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) {
|
||||
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;
|
||||
}
|
||||
export const getLoadedSamples = () => sampleCache.current;
|
||||
|
||||
@@ -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 },
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -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 { fromMidi, logger, toMidi } from '@strudel.cycles/core';
|
||||
import './feedbackdelay.mjs';
|
||||
import './reverb.mjs';
|
||||
const { Pattern, logger } = strudel;
|
||||
import { getSampleBufferSource } from './sampler.mjs';
|
||||
const { Pattern } = strudel;
|
||||
import './vowel.mjs';
|
||||
import workletsUrl from './worklets.mjs?url';
|
||||
import { getFilter, gainNode } from './helpers.mjs';
|
||||
import { map } from 'nanostores';
|
||||
|
||||
export const soundMap = map();
|
||||
export function registerSound(key, onTrigger, data = {}) {
|
||||
soundMap.setKey(key, { onTrigger, data });
|
||||
}
|
||||
export function getSound(s) {
|
||||
return soundMap.get()[s];
|
||||
}
|
||||
export const resetLoadedSounds = () => soundMap.set({});
|
||||
// export const getAudioContext = () => Tone.getContext().rawContext;
|
||||
|
||||
let audioContext;
|
||||
export const getAudioContext = () => {
|
||||
@@ -45,6 +38,65 @@ export const panic = () => {
|
||||
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;
|
||||
function loadWorklets() {
|
||||
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 = {};
|
||||
const maxfeedback = 0.98;
|
||||
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]) {
|
||||
const ac = getAudioContext();
|
||||
const dly = ac.createFeedbackDelay(1, delaytime, delayfeedback);
|
||||
@@ -132,11 +186,15 @@ export const webaudioOutput = async (hap, deadline, hapDuration, cps) => {
|
||||
|
||||
// calculate absolute time
|
||||
let t = ac.currentTime + deadline;
|
||||
// destructure
|
||||
// destructure value
|
||||
let {
|
||||
s = 'triangle',
|
||||
freq,
|
||||
s,
|
||||
bank,
|
||||
source,
|
||||
sf,
|
||||
clip = 0, // if 1, samples will be cut off when the hap ends
|
||||
n = 0,
|
||||
note,
|
||||
gain = 0.8,
|
||||
// low pass
|
||||
cutoff,
|
||||
@@ -152,48 +210,110 @@ export const webaudioOutput = async (hap, deadline, hapDuration, cps) => {
|
||||
crush,
|
||||
shape,
|
||||
pan,
|
||||
speed = 1, // sample playback speed
|
||||
begin = 0,
|
||||
end = 1,
|
||||
vowel,
|
||||
delay = 0,
|
||||
delayfeedback = 0.5,
|
||||
delaytime = 0.25,
|
||||
unit,
|
||||
nudge = 0, // TODO: is this in seconds?
|
||||
cut,
|
||||
loop,
|
||||
orbit = 1,
|
||||
room,
|
||||
size = 2,
|
||||
} = hap.value;
|
||||
const { velocity = 1 } = hap.context;
|
||||
gain *= velocity; // legacy fix for velocity
|
||||
let toDisconnect = []; // audio nodes that will be disconnected when the source has ended
|
||||
const onended = () => {
|
||||
toDisconnect.forEach((n) => n?.disconnect());
|
||||
};
|
||||
// the chain will hold all audio nodes that connect to each other
|
||||
const chain = [];
|
||||
if (bank && s) {
|
||||
s = `${bank}_${s}`;
|
||||
}
|
||||
// get source AudioNode
|
||||
let sourceNode;
|
||||
if (source) {
|
||||
sourceNode = source(t, hap.value, hapDuration);
|
||||
} else if (getSound(s)) {
|
||||
const { onTrigger } = getSound(s);
|
||||
const soundHandle = await onTrigger(t, hap.value, onended);
|
||||
if (soundHandle) {
|
||||
sourceNode = soundHandle.node;
|
||||
soundHandle.stop(t + hapDuration);
|
||||
if (!s || ['sine', 'square', 'triangle', 'sawtooth'].includes(s)) {
|
||||
// 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 } = hap.value;
|
||||
// with synths, n and note are the same thing
|
||||
n = note || n || 36;
|
||||
if (typeof n === 'string') {
|
||||
n = toMidi(n); // e.g. c3 => 48
|
||||
}
|
||||
// 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 {
|
||||
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;
|
||||
// load sample
|
||||
if (speed === 0) {
|
||||
// no playback
|
||||
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) {
|
||||
logger(`[sampler] still loading sound "${s}:${n}"`, 'highlight');
|
||||
// console.warn('sample still loading:', s, n);
|
||||
return;
|
||||
}
|
||||
if (!bufferSource) {
|
||||
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);
|
||||
}
|
||||
if (!sourceNode) {
|
||||
// if onTrigger does not return anything, we will just silently skip
|
||||
// this can be used for things like speed(0) in the sampler
|
||||
return;
|
||||
}
|
||||
if (ac.currentTime > t) {
|
||||
logger('[webaudio] skip hap: still loading', ac.currentTime - t);
|
||||
return;
|
||||
}
|
||||
const chain = []; // audio nodes that will be connected to each other sequentially
|
||||
chain.push(sourceNode);
|
||||
|
||||
// gain stage
|
||||
chain.push(gainNode(gain));
|
||||
@@ -237,9 +357,8 @@ export const webaudioOutput = async (hap, deadline, hapDuration, cps) => {
|
||||
// connect chain elements together
|
||||
chain.slice(1).reduce((last, current) => last.connect(current), chain[0]);
|
||||
|
||||
// toDisconnect = all the node that should be disconnected in onended callback
|
||||
// this is crucial for performance
|
||||
toDisconnect = chain.concat([delaySend, reverbSend]);
|
||||
// disconnect all nodes when source node has ended:
|
||||
chain[0].onended = () => chain.concat([delaySend, reverbSend]).forEach((n) => n?.disconnect());
|
||||
};
|
||||
|
||||
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) ?
|
||||
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 }),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/webdirt",
|
||||
"version": "0.8.0",
|
||||
"version": "0.6.0",
|
||||
"description": "WebDirt integration for Strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
@@ -35,7 +35,7 @@
|
||||
"WebDirt": "github:dktr0/WebDirt"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^4.3.3",
|
||||
"vitest": "^0.28.0"
|
||||
"vite": "^3.2.2",
|
||||
"vitest": "^0.25.7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as strudel from '@strudel.cycles/core';
|
||||
const { Pattern } = strudel;
|
||||
import * as WebDirt from 'WebDirt';
|
||||
//import { loadBuffer, getLoadedBuffer } from '@strudel.cycles/webaudio';
|
||||
import { getLoadedSamples, loadBuffer, getLoadedBuffer } from '@strudel.cycles/webaudio';
|
||||
|
||||
let webDirt;
|
||||
|
||||
@@ -62,9 +62,8 @@ export function loadWebDirt(config) {
|
||||
* @noAutocomplete
|
||||
*/
|
||||
Pattern.prototype.webdirt = function () {
|
||||
throw new Error('webdirt support has been dropped..');
|
||||
// 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) {
|
||||
throw new Error('WebDirt not initialized!');
|
||||
}
|
||||
@@ -93,5 +92,5 @@ Pattern.prototype.webdirt = function () {
|
||||
webDirt.playSample(msg, deadline);
|
||||
}
|
||||
}
|
||||
}); */
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/xen",
|
||||
"version": "0.8.0",
|
||||
"version": "0.6.0",
|
||||
"description": "Xenharmonic API for strudel",
|
||||
"main": "index.mjs",
|
||||
"publishConfig": {
|
||||
@@ -33,7 +33,7 @@
|
||||
"@strudel.cycles/core": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^4.3.3",
|
||||
"vitest": "^0.28.0"
|
||||
"vite": "^3.2.2",
|
||||
"vitest": "^0.25.7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,597 @@
|
||||
[
|
||||
{
|
||||
"id": "toussaintEuclideanAlgorithmGenerates2005",
|
||||
"type": "paper-conference",
|
||||
"abstract": "The Euclidean algorithm (which comes down to us from Euclid's Elements) computes the greatest common divisor of two given integers. It is shown here that the structure of the Euclidean algorithm may be used to automatically generate, very efficiently, a large family of rhythms used as timelines (ostinatos), in traditional world music. These rhythms, here dubbed Euclidean rhythms, have the property that their onset patterns are distributed as evenly as possible in a mathematically precise sense, and optimal manner.",
|
||||
"container-title": "In Proceedings of BRIDGES: Mathematical Connections in Art, Music and Science",
|
||||
"page": "47–56",
|
||||
"title": "The Euclidean Algorithm Generates Traditional Musical Rhythms",
|
||||
"URL": "http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.62.231",
|
||||
"author": [
|
||||
{
|
||||
"family": "Toussaint",
|
||||
"given": "Godfried"
|
||||
}
|
||||
],
|
||||
"issued": {
|
||||
"date-parts": [
|
||||
[
|
||||
"2005"
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "roberts2015",
|
||||
"type": "paper-conference",
|
||||
"abstract": "We describe research extending the interactive affordances of textual code fragments in creative coding environments. In particular we examine the potential of source code both to display the state of running processes and also to alter state using means other than traditional text editing. In contrast to previous research that has focused on the inclusion of additional interactive widgets inside or alongside text editors, our research adds a parsing stage to the runtime evaluation of code fragments and imparts additional interactive capabilities on the source code itself. After implementing various techniques in the creative coding environment Gibber, we evaluate our research through a survey on the various methods of visual feedback provided by our research. In addition to results quantifying preferences for certain techniques over others, we found near unanimous support among survey respondents for including similar techniques in other live coding environments.",
|
||||
"collection-title": "NIME 2015",
|
||||
"container-title": "Proceedings of the international conference on New Interfaces for Musical Expression",
|
||||
"event-place": "Baton Rouge, Louisiana, USA",
|
||||
"ISBN": "978-0-692-49547-6",
|
||||
"page": "126–131",
|
||||
"publisher": "The School of Music and the Center for Computation and Technology (CCT), Louisiana State University",
|
||||
"publisher-place": "Baton Rouge, Louisiana, USA",
|
||||
"source": "ACM Digital Library",
|
||||
"title": "Beyond Editing: Extended Interaction with Textual Code Fragments",
|
||||
"title-short": "Beyond Editing",
|
||||
"author": [
|
||||
{
|
||||
"family": "Roberts",
|
||||
"given": "Charles"
|
||||
},
|
||||
{
|
||||
"family": "Wright",
|
||||
"given": "Matthew"
|
||||
},
|
||||
{
|
||||
"family": "Kuchera-Morin",
|
||||
"given": "JoAnn"
|
||||
}
|
||||
],
|
||||
"accessed": {
|
||||
"date-parts": [
|
||||
[
|
||||
"2021",
|
||||
1,
|
||||
27
|
||||
]
|
||||
]
|
||||
},
|
||||
"issued": {
|
||||
"date-parts": [
|
||||
[
|
||||
"2015",
|
||||
5,
|
||||
30
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "rohrhuberSuperDirt2022",
|
||||
"type": "software",
|
||||
"abstract": "Tidal Audio Engine",
|
||||
"genre": "SuperCollider",
|
||||
"license": "GPL-2.0",
|
||||
"note": "original-date: 2015-11-03T20:18:44Z",
|
||||
"publisher": "musikinformatik",
|
||||
"source": "GitHub",
|
||||
"title": "SuperDirt",
|
||||
"URL": "https://github.com/musikinformatik/SuperDirt",
|
||||
"author": [
|
||||
{
|
||||
"family": "Rohrhuber",
|
||||
"given": "Julian"
|
||||
}
|
||||
],
|
||||
"accessed": {
|
||||
"date-parts": [
|
||||
[
|
||||
"2022",
|
||||
6,
|
||||
24
|
||||
]
|
||||
]
|
||||
},
|
||||
"issued": {
|
||||
"date-parts": [
|
||||
[
|
||||
"2022",
|
||||
6,
|
||||
24
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "mcleanAlgorithmicPattern2020",
|
||||
"type": "paper-conference",
|
||||
"abstract": "This paper brings together two main perspectives on algorithmic pattern. First, the writing of musical patterns in live coding performance, and second, the weaving of patterns in textiles. In both cases, algorithmic pattern is an interface between the human and the outcome, where small changes have far-reaching impact on the results. By bringing contemporary live coding and ancient textile approaches together, we reach a common view of pattern as algorithmic movement (e.g. looping, shifting, reflecting, interfering) in the making of things. This works beyond the usual definition of pattern used in musical interfaces, of mere repeating sequences. We conclude by considering the place of algorithmic pattern in a wider activity of making.",
|
||||
"container-title": "Proceedings of the International Conference on New Interfaces for Musical Expression",
|
||||
"event-place": "Birmingham, UK",
|
||||
"event-title": "NIME2020",
|
||||
"page": "265--270",
|
||||
"publisher-place": "Birmingham, UK",
|
||||
"title": "Algorithmic Pattern",
|
||||
"URL": "https://zenodo.org/record/4813352",
|
||||
"author": [
|
||||
{
|
||||
"family": "Mclean",
|
||||
"given": "Alex"
|
||||
}
|
||||
],
|
||||
"accessed": {
|
||||
"date-parts": [
|
||||
[
|
||||
"2022",
|
||||
1,
|
||||
9
|
||||
]
|
||||
]
|
||||
},
|
||||
"issued": {
|
||||
"date-parts": [
|
||||
[
|
||||
"2020",
|
||||
6,
|
||||
1
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "mcleanFeedforward2020",
|
||||
"type": "paper-conference",
|
||||
"abstract": "This is an improvised, from-scratch live coding performance. The NIME interface which this performance showcases is the new Feedfoward editor for the TidalCycles live coding environment. Feedforward is written in Haskell using the ncurses library for terminal-based user interfaces. It runs on low-powered hardware including the Raspberry Pi Zero, with formative testing of prototypes conducted with several groups of children between the ages of 8 and 14. Feedforward has a number of features designed to support improvised, multi-pattern live coding. Individual Tidal patterns are addressable with hotkeys for fast mute and unmuting. Each pattern has a stereo VU meter, to aid the quick matching of sound to pattern within a mix. In addition, TidalCycles has been extended to store context with each event, so that source code positions in its polyrhythmic sequence mini-notation are tracked. This allows steps to be highlighted in the source code when- ever they are active. This works even when Tidal combinators have been applied to manipulate the timeline. Formal evaluation has yet to take place, but this feature appears to support learning of how pattern manipulations work in Tidal. Feedforward and TidalCycles is free/open source software under a GPL licence version 3.0.",
|
||||
"container-title": "Proceedings of New Interfaces for Musical Expression",
|
||||
"event-place": "Birmingham",
|
||||
"event-title": "NIME2020",
|
||||
"publisher-place": "Birmingham",
|
||||
"title": "Feedforward",
|
||||
"URL": "https://zenodo.org/record/6353969",
|
||||
"author": [
|
||||
{
|
||||
"family": "McLean",
|
||||
"given": "Alex"
|
||||
}
|
||||
],
|
||||
"accessed": {
|
||||
"date-parts": [
|
||||
[
|
||||
"2022",
|
||||
6,
|
||||
24
|
||||
]
|
||||
]
|
||||
},
|
||||
"issued": {
|
||||
"date-parts": [
|
||||
[
|
||||
"2020",
|
||||
7,
|
||||
21
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "robertsGibberLiveCoding2012",
|
||||
"type": "paper-conference",
|
||||
"abstract": "We present Gibber: a live coding environment for web browsers. Gibber performances are written in pure Java-Script with no syntactical additions or modifications; this enables Gibber code to be executed in any web page viewed inside a browser implementing a realtime audio API. Gib-ber offers an array of synthesis options (FM, granular, subtractive, physical modeling), audio effects and sequenc-ing objects to control them. The Gibber environment en-ables simple networked performances where multiple users simultaneously control a remote instance of Gibber. We strove to make the syntax of Gibber clear and concise; when coupled with the ability to run examples in any web page this gives Gibber interesting possibilities as an edu-cational tool. 1.",
|
||||
"container-title": "In Proceedings of the 2012 International Computer Music Conference",
|
||||
"source": "CiteSeer",
|
||||
"title": "Gibber: Live coding audio in the browser",
|
||||
"title-short": "Gibber",
|
||||
"author": [
|
||||
{
|
||||
"family": "Roberts",
|
||||
"given": "Charles"
|
||||
},
|
||||
{
|
||||
"family": "Kuchera-morin",
|
||||
"given": "Joann"
|
||||
}
|
||||
],
|
||||
"issued": {
|
||||
"date-parts": [
|
||||
[
|
||||
"2012"
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "ogbornEstuaryBrowserbasedCollaborative2017",
|
||||
"type": "paper-conference",
|
||||
"abstract": "This paper describes the initial design and development of Estuary, a browser-based collaborative projectional editing environment built on top of the popular TidalCycles language for the live coding of musical pattern. Key features of Estuary include a strict form of structure editing (making syntactical errors impossible), a click-only border-free approach to interface design, explicit notations to modulate the liveness of different parts of the code, and a server-based network collaboration system that can be used for many simultaneous collaborative live coding performances, as well as to present different views of the same live coding activity. Estuary has been developed using Reflex-DOM, a Haskell-based framework for web development whose strictness promises robustness and security advantages.",
|
||||
"container-title": "Proceedings of the International Conference on Live Coding",
|
||||
"event-place": "Morelia",
|
||||
"event-title": "ICLC2017",
|
||||
"language": "en",
|
||||
"page": "11",
|
||||
"publisher-place": "Morelia",
|
||||
"source": "Zotero",
|
||||
"title": "Estuary: Browser-based Collaborative Projectional Live Coding of Musical Patterns",
|
||||
"author": [
|
||||
{
|
||||
"family": "Ogborn",
|
||||
"given": "David"
|
||||
},
|
||||
{
|
||||
"family": "Beverley",
|
||||
"given": "Jamie"
|
||||
},
|
||||
{
|
||||
"family": "Navarro del Angel",
|
||||
"given": "Luis"
|
||||
},
|
||||
{
|
||||
"family": "Tsabary",
|
||||
"given": "Eldad"
|
||||
},
|
||||
{
|
||||
"family": "McLean",
|
||||
"given": "Alex"
|
||||
}
|
||||
],
|
||||
"issued": {
|
||||
"date-parts": [
|
||||
[
|
||||
"2017"
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "mcleanTidalVortexZero2022",
|
||||
"type": "paper-conference",
|
||||
"abstract": "In this paper we introduce ‘version zero’ of TidalVortex, an alternative implementation of the TidalCycles live coding system, using the Python programming language. This is open-ended work, exploring what happens when we try to extract the 'essence' of a system like TidalCycles and translate it into another programming language, while taking advantage of the affordance of its new host. First, we review the substantial prior art in porting TidalCycles, and in representing musical patterns in Python. We then compare equivalent patterns written in Haskell (TidalCycles) and Python (TidalVortex), and relate implementation details of how functional reactive paradigms have translated from the pure functional, strongly typed Haskell to the more multi-paradigm, dynamically typed Python. Finally, we conclude with reflections and generalisable outcomes.",
|
||||
"container-title": "Proceedings of the 7th International Conference on Live Coding",
|
||||
"DOI": "10.5281/zenodo.6456380",
|
||||
"event-place": "Limerick, Ireland",
|
||||
"event-title": "ICMC2022",
|
||||
"note": "event-title: International Conference on Computer Music (ICMC)",
|
||||
"publisher": "Zenodo",
|
||||
"publisher-place": "Limerick, Ireland",
|
||||
"title": "TidalVortex Zero",
|
||||
"URL": "https://zenodo.org/record/6456380/export/hx",
|
||||
"author": [
|
||||
{
|
||||
"family": "McLean",
|
||||
"given": "Alex"
|
||||
},
|
||||
{
|
||||
"family": "Forment",
|
||||
"given": "Raphaël"
|
||||
},
|
||||
{
|
||||
"family": "Le Beux",
|
||||
"given": "Sylvain"
|
||||
},
|
||||
{
|
||||
"family": "Silvani",
|
||||
"given": "Damián"
|
||||
}
|
||||
],
|
||||
"accessed": {
|
||||
"date-parts": [
|
||||
[
|
||||
"2022",
|
||||
6,
|
||||
24
|
||||
]
|
||||
]
|
||||
},
|
||||
"issued": {
|
||||
"date-parts": [
|
||||
[
|
||||
"2022",
|
||||
4,
|
||||
12
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "solomonPurescriptocarina2022",
|
||||
"type": "software",
|
||||
"abstract": "Web audio graphs as a stream",
|
||||
"genre": "PureScript",
|
||||
"license": "Apache-2.0",
|
||||
"note": "original-date: 2021-03-25T12:19:55Z",
|
||||
"source": "GitHub",
|
||||
"title": "purescript-ocarina",
|
||||
"URL": "https://github.com/mikesol/purescript-ocarina",
|
||||
"author": [
|
||||
{
|
||||
"family": "Solomon",
|
||||
"given": "Mike"
|
||||
}
|
||||
],
|
||||
"accessed": {
|
||||
"date-parts": [
|
||||
[
|
||||
"2022",
|
||||
6,
|
||||
24
|
||||
]
|
||||
]
|
||||
},
|
||||
"issued": {
|
||||
"date-parts": [
|
||||
[
|
||||
"2022",
|
||||
6,
|
||||
20
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "orlareyFaustProgrammingLanguage2023",
|
||||
"type": "software",
|
||||
"abstract": "Functional programming language for signal processing and sound synthesis",
|
||||
"genre": "C++",
|
||||
"note": "original-date: 2016-11-05T13:09:11Z",
|
||||
"publisher": "GRAME",
|
||||
"source": "GitHub",
|
||||
"title": "Faust - Programming Language for Audio Applications and Plugins",
|
||||
"URL": "https://github.com/grame-cncm/faust",
|
||||
"author": [
|
||||
{
|
||||
"family": "Orlarey",
|
||||
"given": "Yann"
|
||||
},
|
||||
{
|
||||
"family": "Fober",
|
||||
"given": "Dominique"
|
||||
},
|
||||
{
|
||||
"family": "Letz",
|
||||
"given": "Stéphane"
|
||||
}
|
||||
],
|
||||
"accessed": {
|
||||
"date-parts": [
|
||||
[
|
||||
"2023",
|
||||
3,
|
||||
14
|
||||
]
|
||||
]
|
||||
},
|
||||
"issued": {
|
||||
"date-parts": [
|
||||
[
|
||||
"2023",
|
||||
3,
|
||||
13
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "jackHydra2023",
|
||||
"type": "software",
|
||||
"abstract": "Livecoding networked visuals in the browser",
|
||||
"genre": "JavaScript",
|
||||
"license": "AGPL-3.0",
|
||||
"note": "original-date: 2022-06-13T16:57:34Z",
|
||||
"source": "GitHub",
|
||||
"title": "Hydra",
|
||||
"URL": "https://github.com/ojack/hydra",
|
||||
"author": [
|
||||
{
|
||||
"family": "Jack",
|
||||
"given": "Olivia"
|
||||
}
|
||||
],
|
||||
"accessed": {
|
||||
"date-parts": [
|
||||
[
|
||||
"2023",
|
||||
3,
|
||||
14
|
||||
]
|
||||
]
|
||||
},
|
||||
"issued": {
|
||||
"date-parts": [
|
||||
[
|
||||
"2023",
|
||||
3,
|
||||
8
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "lanGlicol2023",
|
||||
"type": "software",
|
||||
"abstract": "Graph-oriented live coding language and music DSP library written in Rust",
|
||||
"genre": "Rust",
|
||||
"license": "MIT",
|
||||
"note": "original-date: 2020-07-13T22:13:33Z",
|
||||
"source": "GitHub",
|
||||
"title": "Glicol",
|
||||
"URL": "https://github.com/chaosprint/glicol",
|
||||
"author": [
|
||||
{
|
||||
"family": "Lan",
|
||||
"given": "Qichao"
|
||||
}
|
||||
],
|
||||
"accessed": {
|
||||
"date-parts": [
|
||||
[
|
||||
"2023",
|
||||
3,
|
||||
14
|
||||
]
|
||||
]
|
||||
},
|
||||
"issued": {
|
||||
"date-parts": [
|
||||
[
|
||||
"2023",
|
||||
3,
|
||||
14
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "ogbornWebDirt2022",
|
||||
"type": "software",
|
||||
"abstract": "Sampling engine implemented in Web Audio API (rough re-creation of Dirt)",
|
||||
"genre": "JavaScript",
|
||||
"license": "GPL-3.0",
|
||||
"note": "original-date: 2016-05-04T21:19:04Z",
|
||||
"source": "GitHub",
|
||||
"title": "WebDirt",
|
||||
"URL": "https://github.com/dktr0/WebDirt",
|
||||
"author": [
|
||||
{
|
||||
"family": "Ogborn",
|
||||
"given": "David"
|
||||
}
|
||||
],
|
||||
"accessed": {
|
||||
"date-parts": [
|
||||
[
|
||||
"2023",
|
||||
3,
|
||||
14
|
||||
]
|
||||
]
|
||||
},
|
||||
"issued": {
|
||||
"date-parts": [
|
||||
[
|
||||
"2022",
|
||||
12,
|
||||
14
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "roosStrudelAlgorithmicPatterns2022",
|
||||
"type": "paper-conference",
|
||||
"abstract": "This paper introduces Strudel (or sometimes StrudelCycles), an alternative implementation of the Tidal (or Tidal-Cycles) live coding system, using the JavaScript programming language. Strudel is an attempt to make live coding more accessible, by creating a system that runs entirely in the browser, while opening Tidals approach to algorithmic patterns (Mclean 2020) up to modern audio/visual web technologies. The Strudel REPL is a live code editor dedicated to manipulating Strudel patterns while they play, with builtin visual feedback. While Strudel is written in JavaScript, the API is optimized for simplicity and readability by applying code transformations on the syntax tree level, allowing language operations that would otherwise be impossible. The application supports multiple ways to output sound, including Tone.js, Web Audio nodes, OSC (Open Sound Control) messages, Web Serial andWeb MIDI. The project is split into multiple packages, allowing granular reuse in other applications. Apart from TidalCycles, Strudel draws inspiration from many prior existing projects like TidalVortex (McLean et al. 2022), Gibber (Roberts and Kuchera-morin 2012), Estuary (Ogborn et al. 2017), Hydra (Jack [2022] 2022), Ocarina (Solomon [2021] 2022) and Feedforward (McLean 2020).",
|
||||
"container-title": "Proceedings of Web Audio Conference",
|
||||
"event-place": "Cannes, France",
|
||||
"event-title": "WAC2022",
|
||||
"note": "DOI: 10.5281/zenodo.6821023\npublisher: Zenodo",
|
||||
"publisher-place": "Cannes, France",
|
||||
"title": "Strudel: Algorithmic Patterns for the Web",
|
||||
"URL": "https://zenodo.org/record/6821023",
|
||||
"author": [
|
||||
{
|
||||
"family": "Roos",
|
||||
"given": "Felix"
|
||||
},
|
||||
{
|
||||
"family": "McLean",
|
||||
"given": "Alex"
|
||||
}
|
||||
],
|
||||
"accessed": {
|
||||
"date-parts": [
|
||||
[
|
||||
"2022",
|
||||
11,
|
||||
29
|
||||
]
|
||||
]
|
||||
},
|
||||
"issued": {
|
||||
"date-parts": [
|
||||
[
|
||||
"2022",
|
||||
6,
|
||||
28
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "yiWebAssemblyAudioWorkletCsound2018",
|
||||
"type": "paper-conference",
|
||||
"abstract": "This paper describes WebAssembly AudioWorklet (WAAW)\nCsound, one of the implementations of Web Audio Csound.\nWe begin by introducing the background to this current implementation, stemming from the two first ports of Csound\nto the web platform using Native Clients and asm.js. The\ntechnology of WebAssembly is then introduced and discussed in its more relevant aspects. The AudioWorklet interface of Web Audio API is explored, together with its use in\nWAAW Csound. We complement this discussion by considering the overarching question of support for multiple platforms, which implement different versions of Web Audio.\nSome initial examples of the system are presented to illustrate various potential applications. Finally, we complement\nthe paper by discussing current issues that are fundamental\nfor this project and others that rely on the development of\na robust support for WASM-based audio computing.",
|
||||
"event-place": "Berlin, Germany",
|
||||
"event-title": "Web Audio Conference WAC-2018",
|
||||
"language": "en",
|
||||
"publisher-place": "Berlin, Germany",
|
||||
"source": "mural.maynoothuniversity.ie",
|
||||
"title": "WebAssembly AudioWorklet Csound",
|
||||
"URL": "https://mural.maynoothuniversity.ie/16018/",
|
||||
"author": [
|
||||
{
|
||||
"family": "Yi",
|
||||
"given": "Steven"
|
||||
},
|
||||
{
|
||||
"family": "Lazzarini",
|
||||
"given": "Victor"
|
||||
},
|
||||
{
|
||||
"family": "Costello",
|
||||
"given": "Edward"
|
||||
}
|
||||
],
|
||||
"accessed": {
|
||||
"date-parts": [
|
||||
[
|
||||
"2023",
|
||||
3,
|
||||
14
|
||||
]
|
||||
]
|
||||
},
|
||||
"issued": {
|
||||
"date-parts": [
|
||||
[
|
||||
"2018"
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "majdaPeggy2023",
|
||||
"type": "software",
|
||||
"abstract": "Peggy: Parser generator for JavaScript",
|
||||
"genre": "JavaScript",
|
||||
"license": "MIT",
|
||||
"note": "original-date: 2020-11-21T11:52:39Z",
|
||||
"publisher": "peggyjs",
|
||||
"source": "GitHub",
|
||||
"title": "Peggy",
|
||||
"URL": "https://github.com/peggyjs/peggy",
|
||||
"author": [
|
||||
{
|
||||
"family": "Majda",
|
||||
"given": "David"
|
||||
}
|
||||
],
|
||||
"accessed": {
|
||||
"date-parts": [
|
||||
[
|
||||
"2023",
|
||||
3,
|
||||
14
|
||||
]
|
||||
]
|
||||
},
|
||||
"issued": {
|
||||
"date-parts": [
|
||||
[
|
||||
"2023",
|
||||
3,
|
||||
14
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -8,7 +8,7 @@ author:
|
||||
affiliation: Then Try This
|
||||
email: alex@slab.org
|
||||
abstract: |
|
||||
This paper introduces Strudel, which brings the TidalCycles approach to live coding algorithmic patterns to native JavaScript and the web. We begin by giving a little background of the first year of development, before sharing some detail about its implementation and examples of use. We go on to outline the wide range of synthesis and other outputs available in Strudel, including WebAudio, MIDI, OSC (for SuperDirt), WebSerial and CSound, and introduce Strudel's REPL live editor, including its built-in visualisations. We then compare Strudel with Tidal, the trade-offs involved between JavaScript and Haskell, and the unique capabilities offered by Strudel for aligning patterns, before concluding with some thoughts about the future.
|
||||
This paper introduces Strudel, which faithfully ports the TidalCycles approach to live coding algorithmic patterns to native JavaScript and the web. We begin by giving a little background of the first year of development, before sharing some detail about its implementation and examples of use. We go on to outline the wide range of synthesis and other outputs available in Strudel, including WebAudio, MIDI, OSC (for SuperDirt), WebSerial and CSound, and introduce Strudel's REPL live editor, including its built-in visualisations. We then compare Strudel with Tidal, the trade-offs involved between JavaScript and Haskell, and the unique capabilities offered by Strudel for aligning patterns, before concluding with some thoughts about the future.
|
||||
bibliography: citations.json
|
||||
fontsize: 11pt
|
||||
geometry: margin=2cm
|
||||
@@ -23,37 +23,36 @@ date: '2022-12-14'
|
||||
|
||||
In the following paper, we introduce *Strudel*, an alternative
|
||||
implementation of the TidalCycles (or 'Tidal' for short) live coding
|
||||
system, using the JavaScript programming language. Strudel is an
|
||||
system. Whereas Tidal is implemented in the pure functional Haskell programming language, Strudel brings it to the multi-paradigm, web-centric JavaScript language. Strudel is an
|
||||
attempt to make live coding more accessible, by creating a system that
|
||||
runs entirely in the browser, while opening Tidal's approach to
|
||||
algorithmic patterns [@mcleanAlgorithmicPattern2020a] up to modern
|
||||
can run entirely in the browser, while opening Tidal's approach to
|
||||
algorithmic patterns [@mcleanAlgorithmicPattern2020] up to modern
|
||||
audio/visual web technologies. The Strudel REPL is a live code editor
|
||||
dedicated to manipulating patterns while they play, with builtin
|
||||
visual feedback. While Strudel is written in JavaScript, the API is
|
||||
optimized for simplicity and readability by applying code
|
||||
visual feedback. While Strudel is written in standard JavaScript, it does support some optimisations for simplicity, readability and editability by applying code
|
||||
transformations on the syntax tree level, allowing language operations
|
||||
that would otherwise be impossible. The application supports multiple
|
||||
ways to output sound, including Tone.js, Web Audio Nodes, OSC (Open
|
||||
Sound Control) messages, Web Serial, Web MIDI and Csound. The project
|
||||
is split into multiple packages, allowing granular reuse in other
|
||||
applications. Apart from TidalCycles, Strudel draws inspiration from
|
||||
many prior existing projects like TidalVortex
|
||||
several other existing projects like TidalVortex
|
||||
[@mcleanTidalVortexZero2022], Gibber [@robertsGibberLiveCoding2012],
|
||||
Estuary [@ogbornEstuaryBrowserbasedCollaborative2017], Hydra
|
||||
[@jackHydra2022], Ocarina [@solomonPurescriptocarina2022] and
|
||||
Feedforward [@mcleanFeedforward2020]. This paper expands the Strudel
|
||||
Demo paper for the Web Audio Conference 2022 [@StrudelWAC2022].
|
||||
[@jackHydra2023], Ocarina [@solomonPurescriptocarina2022] and
|
||||
Feedforward [@mcleanFeedforward2020]. This paper builds upon a
|
||||
demo paper presented at Web Audio Conference 2022 [@roosStrudelAlgorithmicPatterns2022].
|
||||
|
||||
The first tentative commit to the Strudel project was on 22nd January
|
||||
2022 by Alex McLean, with the core representation implemented over the
|
||||
following few days. Although this was his first attempt at a
|
||||
JavaScript-based application, by 27th January, Alex had managed to
|
||||
upload the initial version to the 'npm' javascript package database,
|
||||
upload an initial version to the 'npm' JavaScript package database,
|
||||
sharing with the wider community for comment. By 4th February, Felix
|
||||
Roos had discovered Strudel and contributed a 'REPL' user interface to
|
||||
it, and then contributed a scheduler the next day, so that Strudel
|
||||
could already make sound. At this point, Alex and Felix shared
|
||||
ownership to the repository, and the project has since proved to be a
|
||||
it, and a scheduler the following day, so that Strudel
|
||||
could already begin to make sound. At this point, Alex and Felix shared
|
||||
ownership of the repository, and the project has since proved to be a
|
||||
productive confluence of Felix's own work into music representation
|
||||
and visualisation, with Alex's experience with making Tidal. Felix has
|
||||
since become the primary contributor to Strudel, with Alex continuing
|
||||
@@ -61,24 +60,24 @@ to jump between developing both Strudel and Tidal. Aspects of
|
||||
Strudel's development have therefore fed back into TidalCycles, and
|
||||
both systems have maintained a shared conceptual underpinning. We plan
|
||||
to continue working towards feature parity between these systems,
|
||||
although within the syntactical trade-offs and library ecosystems of
|
||||
although within the different syntactical trade-offs and library ecosystems of
|
||||
JavaScript and Haskell, some divergence is inevitable and healthy.
|
||||
|
||||
Over the first year of its life, Strudel is now a fully-fledged live
|
||||
coding environment, porting Tidal's core represention of patterns,
|
||||
Now celebrating the first year of its life, Strudel is now a fully-fledged live
|
||||
coding environment, porting Tidal's core representation of patterns,
|
||||
pattern transformations, and mini-notation for polymetric sequences,
|
||||
combined with a wealth of features for synthesising and visualising
|
||||
those patterns.
|
||||
|
||||
# From Tidal to Strudel and back
|
||||
|
||||
As mentioned above, the original Tidal is implemented as a domain specific language (DSL) embedded in the Haskell pure functional programming language, and takes advantage of Haskell's terse syntax and advanced, 'strong' type system. JavaScript on the other hand, is a multi-paradigm programming language, with a dynamic type system. Because Tidal leans heavily on many of Haskell's more unique features, it was not always clear that it could meaningfully be ported to a multi-paradigm scripting language. However, this possibility was already demonstrated with an earlier port to Python [TidalVortex; @mcleanTidalVortexZero2022], and we have now successfully implemented Tidal's pure functional representation of patterns in Strudel, including partial application, currying, and the functor, applicative and monadic structures that underlie Tidal's expressive pattern transformations. The result is a terse and highly composable system, where everything is either a pattern, or a function for combining and manipulating patterns, offering a rich creative ground for exploration.
|
||||
As mentioned earlier, the original Tidal is implemented as a domain specific language (DSL) embedded in the Haskell pure functional programming language, and takes advantage of Haskell's terse syntax and advanced, 'strong' type system. JavaScript on the other hand, is a multi-paradigm programming language, with a dynamic type system. Because Tidal leans heavily on many of Haskell's more unique features, it was not always clear that it could meaningfully be ported to a multi-paradigm scripting language. However, this possibility was already demonstrated with an earlier port to Python [TidalVortex; @mcleanTidalVortexZero2022], and we have now successfully implemented Tidal's pure functional representation of patterns in Strudel, including partial application, currying, and the functor, applicative and monadic structures that underlie Tidal's expressive pattern transformations. The result is a terse and highly composable system, where everything is either a pattern, or a function for combining and manipulating patterns, offering a rich creative ground for exploration.
|
||||
|
||||
This development process has been far from a one-way port, however. The process of porting Tidal's concepts has also opened up new possibilities, some just from revisiting every design decision, and some from the particular affordances and constraints offered by JavaScript. This has lead to new features (and indeed bugfixes) that have found their way back to Tidal where appropriate, and ongoing work that we will return to in the conclusion of this paper.
|
||||
|
||||
# Representing Patterns
|
||||
|
||||
Patterns are the essence of Tidal. Its patterns are abstract entities that represent flows of time as functions, adapting a technique called pure functional reactive programming. Taking a time span as its input, a Pattern can output a set of events that happen within that time span. It depends on the structure of the Pattern how the events are located in time.
|
||||
Patterns are the essence of Tidal. Its patterns are abstract entities that represent flows of time as functions, adapting a technique called pure functional reactive programming. Taking a time span as its input, a Pattern will output a set of events that happen within that time span. It depends on the structure of the Pattern how the events are located in time.
|
||||
From now on, this process of generating events from a time span will be called **querying**.
|
||||
Example:
|
||||
|
||||
@@ -101,7 +100,7 @@ The resulting events are:
|
||||
|
||||
Each event has a value, a begin time and an end time, where time is represented as a fraction. In the above case, the events are placed in sequential order, where c3 takes the first half, and e3 and g3 together take the second half. This temporal placement is the result of the `sequence` function, which divides its arguments equally over one cycle. If an argument is an array, the same rule applies to that part of the cycle. In the example, e3 and g3 are divided equally over the second half of the whole cycle.
|
||||
|
||||
Note that the query function is not just a way to access a pattern, but true to the principles of functional programming, is the pattern itself. This means that in theory there is no way to change a pattern, it is opaque as a pure function. In practice though, Strudel and Tidal are all about transforming patterns, so how is this done? The answer is, by replacing the pattern with a new one, that calls the old one. This new one is only able to manipulate the query before passing it to the old pattern, and manipulate the results from it before returning them to caller. But, this is enough to support all the temporal and structural manipulations provided by Strudel (and Tidal's) extensive library of functions.
|
||||
Note that the query function is not just a way to access a pattern, but true to the principles of functional programming, is the pattern itself. This means that in theory there is no way to change a pattern, it is opaque as a pure function. In practice though, Strudel and Tidal are all about transforming patterns, so how is this done? The answer is, by replacing the pattern with a new one, that calls the old one. This new function is only able to manipulate the query before passing it to the old pattern, and manipulate the results from it before returning them to caller. But, this is enough to support all the temporal and structural manipulations provided by Strudel (and Tidal's) extensive library of functions.
|
||||
|
||||
The above examples do not represent how Strudel is used in practice. In the live coding editor, the user only has to type in the pattern itself, the querying will be handled by the scheduler. The scheduler will repeatedly query the pattern for events, which are then scheduled as sound synthesis or other event triggers.
|
||||
Also, the above event data structure has been simplified for readability.
|
||||
@@ -110,7 +109,7 @@ Also, the above event data structure has been simplified for readability.
|
||||
|
||||
# Making Patterns
|
||||
|
||||
In practice, the end-user live coder will not deal with constructing patterns directly, but will rather build patterns using Strudel's extensive combinator library to create, combine and transform patterns.
|
||||
In practice, the end-user live coder will not deal with constructing patterns directly, but will rather build patterns using Strudel's extensive combinator library, to create, combine and transform patterns.
|
||||
|
||||
The live coder will rarely use the `sequence` function as seen above, as sequencing is implicit in many functions. For example in the following, the `note` function constructs a pattern of notes, sequencing its arguments in the same manner as the previous example.
|
||||
|
||||
@@ -118,29 +117,29 @@ The live coder will rarely use the `sequence` function as seen above, as sequenc
|
||||
note(c3, [e3, g3])
|
||||
```
|
||||
|
||||
Perhaps more often, they will use the mini-notation for even terser notation of rhythmic sequences: ^[This last example is also valid Tidal code, albeit the parenthesis is not required in its Haskell syntax in this case. Tidal does not support passing sequences as lists directly to the `note` function, however.].
|
||||
Perhaps more often, they will use the mini-notation for even terser notation of rhythmic sequences: ^[This example is also valid Tidal code, albeit the parenthesis is not required in its Haskell syntax in this case. Tidal does not support passing sequences as lists directly to the `note` function, however.].
|
||||
|
||||
```js
|
||||
note("c3 [e3 g3]")
|
||||
```
|
||||
|
||||
Such sequences are often treated only as a starting point for manipulation, where they then undergo pattern transformations such as repetition, symmetry, interference/combination or randomisation, potentially at multiple timescales. Because Strudel patterns are represented as pure functions of time rather than as data structures, very long and complex generative results can be represented and manipulated without having to store the resulting sequences in memory.
|
||||
Such sequences are often treated only as a starting point for manipulation, where functions are then applied which represent pattern transformations such as repetition, symmetry, interference/combination or randomisation, potentially at multiple timescales. Because Strudel patterns are represented as pure functions of time rather than as data structures, very long and complex generative results can be represented and manipulated without having to store the resulting sequences in memory.
|
||||
|
||||
# Pattern Example
|
||||
|
||||
The following example showcases how patterns can be utilized to create musical complexity from simple parts, using repetition and interference:
|
||||
The following example showcases how patterns can be utilised to create musical complexity from simple parts, using repetition and interference:
|
||||
|
||||
```js
|
||||
"<0 2 [4 6](3,4,1) 3>"
|
||||
.off(1/4, add(2))
|
||||
.off(1/2, add(6))
|
||||
.scale('D minor')
|
||||
.legato(.25)
|
||||
.scale('D3 minor')
|
||||
.note().s("sawtooth square")
|
||||
.legato(.25)
|
||||
.delay(.8).delaytime(.125)
|
||||
```
|
||||
|
||||
The pattern starts with a rhythm of numbers in mini-notation, which are later interpreted inside the scale of D minor.
|
||||
The pattern starts with a rhythmic pattern of numbers expressed in mini-notation, which are later interpreted inside the scale of D minor.
|
||||
The first line could also be expressed without mini-notation:
|
||||
|
||||
```js
|
||||
@@ -153,16 +152,16 @@ These numbers then undergo various pattern transformations. Here is a short desc
|
||||
- `brackets`: elements inside brackets are divided equally over the time of their parent
|
||||
- `.euclid(p, s, o)`: place p pulses evenly over s steps, with offset o [@toussaintEuclideanAlgorithmGenerates2005]
|
||||
- `.off(n, f)`: layers a pattern on top of itself, with the new layer offset by n cycles, and with function f applied
|
||||
- `.legato(n)`: multiply the duration of all events in a pattern by a factor of n
|
||||
- `.echo(t, n, v)`: copy each event t times, with n cycles in between each copy, decreasing velocity by v
|
||||
- `.note()`: interpretes values as notes
|
||||
- `.scale(name)`: interpretes numbers as indices inside the given scale
|
||||
- `.note()`: interprets values as notes
|
||||
- `.s(name)`: play back each event with the given sound
|
||||
- `.delay(wet)`: add delay
|
||||
- `.legato(n)`: multiply the duration of all events in a pattern by a factor of n
|
||||
- `.delay(amount)`: delay effect send amount
|
||||
- `.delaytime(t)`: set delay time
|
||||
|
||||
Much of the above will be familiar to Tidal users.
|
||||
|
||||
<!-- This example shows some of Strudel's unique support for chords and transposition familiar to students of Western music theory. This differs a little from Tidal's approach and thanks to the integration of the javascript library XXX (*TODO* ? or is this all your work Felix?), Strudel's support for tonal transformations such as voice leading is perhaps respects more advanced than Tidal. -->
|
||||
<!-- This example shows some of Strudel's unique support for chords and transposition familiar to students of Western music theory. This differs a little from Tidal's approach and thanks to the integration of the JavaScript library XXX (*TODO* ? or is this all your work Felix?), Strudel's support for tonal transformations such as voice leading is perhaps respects more advanced than Tidal. -->
|
||||
|
||||
# Ways to make Sound (and other events)
|
||||
|
||||
@@ -181,11 +180,11 @@ While the Web Audio API takes a *fire-and-forget* approach, creating a lot of To
|
||||
|
||||
Strudel's new default output uses the Web Audio API to create a new audio graph for each event. It currently supports basic oscillators, sample playback, various effects and an experimental support for soundfonts.
|
||||
|
||||
WebDirt [@ogbornDktr0WebDirt2022] was created as part of the Estuary Live Coding System [@ogbornEstuaryBrowserbasedCollaborative2017], and proved to be a solid choice for handling samples in Strudel as well. We are however focused on working more directly with the Web Audio API to be able to integrate new features more tightly.
|
||||
WebDirt [@ogbornWebDirt2022] was created as part of the Estuary Live Coding System [@ogbornEstuaryBrowserbasedCollaborative2017], and proved to be a solid choice for handling samples in Strudel as well. We are however focused on working more directly with the Web Audio API to be able to integrate new features more tightly.
|
||||
|
||||
Using the OSC protocol via Strudel's provided Node.js-based OSC proxy server, it is possible to send network messages to trigger events. This is mainly used to render sound using SuperDirt [@SuperDirt2022], which is the well-developed Supercollider-based synthesis framework that Tidal live coders generally use as standard.
|
||||
Using the OSC protocol via Strudel's provided Node.js-based OSC proxy server, it is possible to send network messages to trigger events. This is mainly used to render sound using SuperDirt [@rohrhuberSuperDirt2022], which is the well-developed Supercollider-based synthesis framework used by Tidal live coders as standard.
|
||||
|
||||
Recently, the experimental integration of Csound proved to bring a new dimension of sound design capabilities to Strudel. Thanks to the WebAssembly distribution of this classic system [@CsoundWebAssembly], Csound 'orchestra' synthesisers can be embedded in and then patterned with Strudel code.
|
||||
Recently, the experimental integration of Csound proved to bring a new dimension of sound design capabilities to Strudel. Thanks to the WebAssembly distribution of this classic system [@yiWebAssemblyAudioWorkletCsound2018], Csound 'orchestra' synthesisers can be embedded in and then patterned with Strudel code.
|
||||
|
||||
MIDI output can also be used to send MIDI messages to either external instruments or to other programs on the same device. Unlike OSC, Strudel is able to send MIDI directly without requiring additional proxy software, but only from web browsers that support it (at the time of writing, this means Chromium-based browsers).
|
||||
|
||||
@@ -231,17 +230,13 @@ mini("c3 [e3 g3]*2").withMiniLocation([1,0,0],[1,14,14])
|
||||
|
||||
Here, the string is wrapped in `mini`, which will create a pattern from a mini-notation string. Additionally, the `withMiniLocation` method passes the original source code location of the string to the pattern, which enables highlighting active events.
|
||||
|
||||
Other convenient features like pseudo variables, operator overloading and top level await are possible with transpilation.
|
||||
|
||||
After the transpilation, the code is ready to be evaluated into a `Pattern`.
|
||||
|
||||
Behind the scenes, the user code string is parsed with `acorn`, turning it into an Abstract Syntax Tree (AST). The AST allows changing the structure of the code before generating the transpiled version using `escodegen`.
|
||||
Other convenient features like pseudo variables, operator overloading and top level await are possible with transpilation. After the transpilation, the code is ready to be evaluated into a `Pattern`. Behind the scenes, the user code string is parsed with `acorn`, turning it into an Abstract Syntax Tree (AST). The AST allows changing the structure of the code before generating the transpiled version using `escodegen`.
|
||||
|
||||
### Mini-notation
|
||||
|
||||
While the transpilation allows JavaScript to express Patterns in a less verbose way, it is still preferable to use the mini-notation as a more compact way to express rhythm. Strudel aims to provide the same mini-notation features and syntax as used in Tidal.
|
||||
While the transpilation allows us to express patterns in JavaScript in a less verbose way, it is still preferable to use the mini-notation as a more compact way to express rhythm. Strudel aims to provide the same mini-notation features and syntax as used in Tidal.
|
||||
|
||||
The mini-notation parser is implemented using `peggy`, which allows generating performant parsers for Domain Specific Languages (DSLs) using a concise grammar notation. The generated parser turns the mini-notation string into an AST which is used to call the respective Strudel functions with the given structure. For example, `"c3 [e3 g3]*2"` will result in the following calls:
|
||||
The mini-notation parser is implemented using `peggy` [@majdaPeggy2023], which allows generating performant parsers for Domain Specific Languages (DSLs) using a concise grammar notation. The generated parser turns the mini-notation string into an AST which is used to call the respective Strudel functions with the required structure. For example, `"c3 [e3 g3]*2"` will result in the following calls:
|
||||
|
||||
```js
|
||||
seq(
|
||||
@@ -255,12 +250,12 @@ seq(
|
||||
|
||||
### Highlighting Locations
|
||||
|
||||
As seen in the examples above, both the JS and the mini-notation parser add source code locations using `withMiniLocation` and `withLocation` methods. While the JS parser adds locations relative to the user code as a whole, the mini-notation adds locations relative to the position of the mini-notation string. The absolute location of elements within mini-notation can be calculated by simply adding both locations together. This absolute location can be used to highlight active events in real time.
|
||||
As seen in the examples above, both the JS and the mini-notation parser add source code locations using `withMiniLocation` and `withLocation` methods. While the JS parser adds locations relative to the user code as a whole, the mini-notation adds locations relative to the position of the mini-notation string. The absolute location of elements within mini-notation can be calculated by simply adding both locations together. This absolute location is then used to highlight active events in real time.
|
||||
|
||||
## Scheduling Events
|
||||
|
||||
After an instance of `Pattern` is obtained from the user code,
|
||||
it is used by the scheduler to get queried for events. Once started, the scheduler runs at a fixed interval to query the active pattern for events within the current interval's time span. A simplified implementation looks like this:
|
||||
the scheduler queries it for events. Once started, the scheduler runs at a fixed interval to query the active pattern for events within the current interval's time span. A simplified implementation looks like this:
|
||||
|
||||
```js
|
||||
let pattern = seq('c3', ['e3', 'g3']); // pattern from user
|
||||
@@ -277,7 +272,7 @@ setInterval(() => {
|
||||
}, interval * 1000); // query each "interval" seconds
|
||||
```
|
||||
|
||||
Note that the above code is simplified for illustrative purposes. The actual implementation has to work around imprecise callbacks of `setInterval`. More about the implementation details can be read in [this blog post](https://loophole-letters.vercel.app/web-audio-scheduling).
|
||||
Note that the above code is simplified for illustrative purposes, for example the actual implementation has to work around imprecise callbacks of `setInterval`. ^[Further implementation details of the Strudel scheduler can be read in this blog post: [loophole-letters.vercel.app/web-audio-scheduling](https://loophole-letters.vercel.app/web-audio-scheduling).]
|
||||
|
||||
The fact that `Pattern.queryArc` is a pure function that maps a time span to a set of events allows us to choose any interval we like without changing the resulting output. It also means that when the pattern is changed from outside, the next scheduling callback will work with the new pattern, keeping its clock running.
|
||||
|
||||
@@ -310,7 +305,7 @@ const { x, y } = createParams('x', 'y')
|
||||
x(sine.range(0, 200)).y(cosine.range(0,200))
|
||||
```
|
||||
|
||||
This example creates the custom control parameters `x` and `y` which are then used to form a pattern that descibes the coordinates of a circle.
|
||||
This example creates the custom control parameters `x` and `y` which are then used to form a pattern that describes the coordinates of a circle.
|
||||
|
||||
### Outputs
|
||||
|
||||
@@ -328,13 +323,13 @@ function onTrigger(hap, deadline, duration) {
|
||||
}
|
||||
```
|
||||
|
||||
The above example will create an `OscillatorNode` for each event, where the frequency is controlled by the `note` param. In essence, this is how the WebAudio API output of Strudel works, only with many more parameters to control synths, samples and effects.
|
||||
The above example will create an `OscillatorNode` for each event, where the frequency is controlled by the `note` parameter. In essence, this is how the WebAudio API output of Strudel works, only with many more parameters to control synths, samples and effects.
|
||||
|
||||
# Pattern alignment and combination
|
||||
|
||||
One core aspect of Strudel, inherited from Tidal, is the flexible way that patterns can be combined, irrespective of their structure. Its declarative approach means a live coder does not have to think about the details of *how* this is done, only *what* is to be done.
|
||||
|
||||
As a simple example, consider two number patterns `"0 [1 2] 3"`, and `"10 20"`. The first has three contiguous steps of equal lengths, with the second step broken down into two substeps, giving four events in total. There are a very large number of ways in which the structure of these two patterns could be combined, but the default method in both Strudel and Tidal is to line up the cycles of the two patterns, and then take events from the first pattern and match them with those in the second pattern. Therefore, the following two lines are equivalent:
|
||||
As a simple example, consider two number patterns `"0 [1 2] 3"`, and `"10 20"`. The first has three contiguous steps of equal lengths, the second of which is broken down into two substeps, giving four events in total. The second pattern simply has two steps, taking up half a cycle each. There are a very large number of ways in which the structure of these two patterns could be combined, but the default method in both Strudel and Tidal is to line up the cycles of the two patterns, and then take events from the first pattern and match them with those in the second pattern. Therefore, the following two lines are equivalent:
|
||||
|
||||
```js
|
||||
"0 [1 2] 3".add("10 20")
|
||||
@@ -389,20 +384,20 @@ versus Strudel's simplicity.
|
||||
To demonstrate this, consider the following Tidal pattern:
|
||||
|
||||
```haskell
|
||||
iter 4 $ every 3 (||+ n "10 20") $ (n "0 1 3") # s "triangle" # crush 4
|
||||
iter 4 $ every 3 (||+ n "10 20") $ n "20 21 23" # s "triangle" # crush 4
|
||||
```
|
||||
|
||||
This can be directly translated to the Strudel equivalent:
|
||||
|
||||
```js
|
||||
iter(4, every(3, add.squeeze("10 20"), n("0 1 3").s("triangle").crush(4)))
|
||||
iter(4, every(3, x => x.add.squeeze(n("10 20")), n("20 21 23").s("triangle").crush(4)))
|
||||
```
|
||||
|
||||
Although for a more canonical Strudel expression, we would reorder it
|
||||
as:
|
||||
|
||||
```js
|
||||
n("0 1 3").every(3, add.squeeze("10 20")).iter(4).s("triangle").crush(4)
|
||||
n("20 21 23").every(3, x => x.add.squeeze(n("10 20"))).iter(4).s("triangle").crush(4)
|
||||
```
|
||||
|
||||
The Strudel example uses the `.` method call operator for all
|
||||
@@ -426,17 +421,17 @@ features.
|
||||
|
||||
With Strudel, we have little choice but to embrace the affordances and
|
||||
constraints offered by JavaScript, and while designing a
|
||||
domain-specific language based on method calls is a
|
||||
domain-specific language based on chaining method calls is a
|
||||
challenge, through creative adoption of functional programming
|
||||
techniques like partial application, we are so far very happy with the
|
||||
results. Tidal's functional reactive approach to pattern-making has in
|
||||
general translated well to JavaScript, and opportunities and
|
||||
constraints have overall traded off to create a very approachable and
|
||||
useable live coding environment.
|
||||
constraints have traded off to create a very approachable and
|
||||
usable live coding environment.
|
||||
|
||||
## The trade-off of flexible typing
|
||||
|
||||
We have identified one problem with porting Tidal to JavaScript where we have missed Haskell's strict typing and type inference. In both Tidal and Strudel, time is rational, where any point in time is represented as the ratio of two integers. This allows representation of musical ratios such that are impossible to represent accurately using the more common floating point numbers. However while libraries are available that support rational numbers in JavaScript, the lack of strict typing means that it is easy to implement pattern methods where computationally expensive conversion from floating point to rational numbers are performed late, and therefore often enough to overload the CPUs, due to the large number of iterative calculations required to estimate a ratio for a given floating point number. To mitigate this problem, we might consider moving to TypeScript in the future.
|
||||
We have identified one problem with porting Tidal to JavaScript where we have missed Haskell's strict typing and type inference. In both Tidal and Strudel, time is rational, where any point in time is represented as the ratio of two integers. This allows representation of musical ratios such that are impossible to represent accurately using the more common floating point numbers. However while libraries are available that support rational numbers in JavaScript, the lack of strict typing means that computationally expensive conversion from floating point to rational numbers may be performed late, and therefore often enough to overload the CPUs, due to the large number of iterative calculations required to estimate a ratio for a given floating point number. To mitigate this problem, we might consider moving to TypeScript in the future.
|
||||
|
||||
# Future Outlook
|
||||
|
||||
@@ -446,14 +441,14 @@ The project is still young, with many features on the horizon. As general guidin
|
||||
2. consistent with Tidal's approach to pattern
|
||||
3. modular and extensible
|
||||
|
||||
While Haskell's type system makes it a great language for the ongoing development of Tidal's inner representation of pattern, JavaScript's vibrant ecosystem, flexibility and accessibility makes it a great host for more ad-hoc experiments, including interface design. For the future, it is planned to integrate additional alternative sound engines such as Glicol [@lanChaosprintGlicol2022] and Faust [@FaustProgrammingLanguage2022]. Strudel is already approaching feature parity with Tidal, but there are more Tidal functions to be ported, and work to be done to improve compatibility with Tidal's mini-notation. Tidal version 2.0 is under development, which brings a new representation for sequences to its patterns, which will then be brought to Strudel. Besides sound, other ways to render events are being explored, such as graphical, and choreographic output. We are also looking into alternative ways of editing patterns, including multi-user editing for network music, parsing a novel syntax to escape the constraints of JavaScript, and developing hardware/e-textile interfaces. In summary, there is a lot of fun ahead.
|
||||
While Haskell's type system makes it a great language for the ongoing development of Tidal's inner representation of pattern, JavaScript's vibrant ecosystem, flexibility and accessibility makes it a great host for more ad-hoc experiments, including interface design. For the future, it is planned to integrate additional alternative sound engines such as Glicol [@lanGlicol2023] and Faust [@orlareyFaustProgrammingLanguage2023]. Strudel is already approaching feature parity with Tidal, but there are more Tidal functions to be ported, and work to be done to improve compatibility with Tidal's mini-notation. Tidal version 2.0 is under development, which brings a new representation for sequences to its patterns, which will then be brought to Strudel. Besides sound, other ways to render events are being explored, such as graphical, and choreographic output. We are also looking into alternative ways of editing patterns, including multi-user editing for network music, parsing a novel syntax to escape the constraints of JavaScript, and developing hardware/e-textile interfaces. In summary, there is a lot of fun ahead.
|
||||
|
||||
# Links
|
||||
|
||||
The Strudel REPL is available at <https://strudel.tidalcycles.org>, including an interactive tutorial.
|
||||
The repository is at <https://github.com/tidalcycles/strudel>, all the code is open source under the AGPL-3.0 License.
|
||||
|
||||
# Acknowledgments
|
||||
# Acknowledgements
|
||||
|
||||
Thanks to the Strudel and wider Tidal, live coding, WebAudio and free/open source software communities for inspiration and support. Alex McLean's work on this project is supported by a UKRI Future Leaders Fellowship [grant number MR/V025260/1].
|
||||
|
||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 220 KiB After Width: | Height: | Size: 220 KiB |
|
Before Width: | Height: | Size: 127 KiB After Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 83 KiB |