Merge branch 'main' of github.com:tidalcycles/strudel

This commit is contained in:
alex 2022-06-18 13:19:38 +01:00
commit 363895761e
104 changed files with 4570 additions and 3969 deletions

5
.gitignore vendored
View File

@ -31,4 +31,7 @@ doc
out out
.parcel-cache .parcel-cache
repl_old repl_old
tutorial.rendered.mdx tutorial.rendered.mdx
doc.json
talk/public/EmuSP12
talk/public/samples

View File

@ -32,7 +32,9 @@ Use one of the Communication Channels listed above.
## Improve the Tutorial ## Improve the Tutorial
If you find some weak spots in the [tutorial](https://strudel.tidalcycles.org/), If you find some weak spots in the [tutorial](https://strudel.tidalcycles.org/),
you are welcome to improve them by editing [this file](https://github.com/tidalcycles/strudel/blob/main/repl/src/tutorial/tutorial.mdx). you are welcome to improve them by editing [this file](https://github.com/tidalcycles/strudel/blob/main/tutorial/tutorial.mdx).
This will even work without setting up a development environment, only a github account is required. This will even work without setting up a development environment, only a github account is required.
## Propose a Feature ## Propose a Feature

View File

@ -2,7 +2,7 @@
[![Strudel test status](https://github.com/tidalcycles/strudel/actions/workflows/test.yml/badge.svg)](https://github.com/tidalcycles/strudel/actions) [![Strudel test status](https://github.com/tidalcycles/strudel/actions/workflows/test.yml/badge.svg)](https://github.com/tidalcycles/strudel/actions)
An experiment in making a [Tidal](https://github.com/tidalcycles/tidal/) using web technologies. This is unstable software, please tread carefully. An experiment in making a [Tidal](https://github.com/tidalcycles/tidal/) using web technologies. This software is slowly stabilising, but please continue to tread carefully.
- Try it here: <https://strudel.tidalcycles.org/> - Try it here: <https://strudel.tidalcycles.org/>
- Tutorial: <https://strudel.tidalcycles.org/tutorial/> - Tutorial: <https://strudel.tidalcycles.org/tutorial/>

3875
doc.json

File diff suppressed because it is too large Load Diff

69
package-lock.json generated
View File

@ -2255,6 +2255,10 @@
"resolved": "packages/webaudio", "resolved": "packages/webaudio",
"link": true "link": true
}, },
"node_modules/@strudel.cycles/webdirt": {
"resolved": "packages/webdirt",
"link": true
},
"node_modules/@strudel.cycles/xen": { "node_modules/@strudel.cycles/xen": {
"resolved": "packages/xen", "resolved": "packages/xen",
"link": true "link": true
@ -10815,6 +10819,11 @@
"defaults": "^1.0.3" "defaults": "^1.0.3"
} }
}, },
"node_modules/WebDirt": {
"version": "1.0.0",
"resolved": "git+ssh://git@github.com/dktr0/WebDirt.git#425dc8fd023440d9c61ffdb8642e44e2710faea0",
"license": "ISC"
},
"node_modules/webidl-conversions": { "node_modules/webidl-conversions": {
"version": "6.1.0", "version": "6.1.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz",
@ -11271,7 +11280,7 @@
}, },
"packages/eval": { "packages/eval": {
"name": "@strudel.cycles/eval", "name": "@strudel.cycles/eval",
"version": "0.1.0", "version": "0.1.1",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"@strudel.cycles/core": "^0.1.0", "@strudel.cycles/core": "^0.1.0",
@ -11298,22 +11307,22 @@
}, },
"packages/midi": { "packages/midi": {
"name": "@strudel.cycles/midi", "name": "@strudel.cycles/midi",
"version": "0.1.0", "version": "0.1.1",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"@strudel.cycles/tone": "^0.1.0", "@strudel.cycles/tone": "^0.1.1",
"tone": "^14.7.77", "tone": "^14.7.77",
"webmidi": "^2.5.2" "webmidi": "^2.5.2"
} }
}, },
"packages/mini": { "packages/mini": {
"name": "@strudel.cycles/mini", "name": "@strudel.cycles/mini",
"version": "0.1.0", "version": "0.1.1",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"@strudel.cycles/core": "^0.1.0", "@strudel.cycles/core": "^0.1.0",
"@strudel.cycles/eval": "^0.1.0", "@strudel.cycles/eval": "^0.1.1",
"@strudel.cycles/tone": "^0.1.0" "@strudel.cycles/tone": "^0.1.1"
} }
}, },
"packages/osc": { "packages/osc": {
@ -11326,14 +11335,14 @@
}, },
"packages/react": { "packages/react": {
"name": "@strudel.cycles/react", "name": "@strudel.cycles/react",
"version": "0.1.1", "version": "0.1.2",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"peer": true, "peer": true,
"dependencies": { "dependencies": {
"@codemirror/lang-javascript": "^0.19.0", "@codemirror/lang-javascript": "^0.19.0",
"@strudel.cycles/core": "*", "@strudel.cycles/core": "*",
"@strudel.cycles/eval": "^0.1.0", "@strudel.cycles/eval": "^0.1.1",
"@strudel.cycles/tone": "^0.1.0", "@strudel.cycles/tone": "^0.1.1",
"react-codemirror6": "^1.1.0", "react-codemirror6": "^1.1.0",
"react-hook-inview": "^4.5.0" "react-hook-inview": "^4.5.0"
}, },
@ -11407,7 +11416,7 @@
}, },
"packages/tonal": { "packages/tonal": {
"name": "@strudel.cycles/tonal", "name": "@strudel.cycles/tonal",
"version": "0.1.0", "version": "0.1.1",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"@strudel.cycles/core": "^0.1.0", "@strudel.cycles/core": "^0.1.0",
@ -11432,7 +11441,7 @@
}, },
"packages/tone": { "packages/tone": {
"name": "@strudel.cycles/tone", "name": "@strudel.cycles/tone",
"version": "0.1.0", "version": "0.1.1",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"@strudel.cycles/core": "^0.1.0", "@strudel.cycles/core": "^0.1.0",
@ -11443,15 +11452,24 @@
}, },
"packages/webaudio": { "packages/webaudio": {
"name": "@strudel.cycles/webaudio", "name": "@strudel.cycles/webaudio",
"version": "0.1.0", "version": "0.1.1",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"@strudel.cycles/core": "^0.1.0" "@strudel.cycles/core": "^0.1.0"
} }
}, },
"packages/webdirt": {
"name": "@strudel.cycles/webdirt",
"version": "0.1.0",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@strudel.cycles/core": "^0.1.0",
"WebDirt": "github:dktr0/WebDirt"
}
},
"packages/xen": { "packages/xen": {
"name": "@strudel.cycles/xen", "name": "@strudel.cycles/xen",
"version": "0.1.0", "version": "0.1.1",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"@strudel.cycles/core": "^0.1.0" "@strudel.cycles/core": "^0.1.0"
@ -13281,7 +13299,7 @@
"@strudel.cycles/midi": { "@strudel.cycles/midi": {
"version": "file:packages/midi", "version": "file:packages/midi",
"requires": { "requires": {
"@strudel.cycles/tone": "^0.1.0", "@strudel.cycles/tone": "^0.1.1",
"tone": "^14.7.77", "tone": "^14.7.77",
"webmidi": "^2.5.2" "webmidi": "^2.5.2"
} }
@ -13290,8 +13308,8 @@
"version": "file:packages/mini", "version": "file:packages/mini",
"requires": { "requires": {
"@strudel.cycles/core": "^0.1.0", "@strudel.cycles/core": "^0.1.0",
"@strudel.cycles/eval": "^0.1.0", "@strudel.cycles/eval": "^0.1.1",
"@strudel.cycles/tone": "^0.1.0" "@strudel.cycles/tone": "^0.1.1"
} }
}, },
"@strudel.cycles/osc": { "@strudel.cycles/osc": {
@ -13305,8 +13323,8 @@
"requires": { "requires": {
"@codemirror/lang-javascript": "^0.19.0", "@codemirror/lang-javascript": "^0.19.0",
"@strudel.cycles/core": "*", "@strudel.cycles/core": "*",
"@strudel.cycles/eval": "^0.1.0", "@strudel.cycles/eval": "^0.1.1",
"@strudel.cycles/tone": "^0.1.0", "@strudel.cycles/tone": "^0.1.1",
"@types/react": "^17.0.2", "@types/react": "^17.0.2",
"@types/react-dom": "^17.0.2", "@types/react-dom": "^17.0.2",
"@vitejs/plugin-react": "^1.3.0", "@vitejs/plugin-react": "^1.3.0",
@ -13401,6 +13419,13 @@
"@strudel.cycles/core": "^0.1.0" "@strudel.cycles/core": "^0.1.0"
} }
}, },
"@strudel.cycles/webdirt": {
"version": "file:packages/webdirt",
"requires": {
"@strudel.cycles/core": "^0.1.0",
"WebDirt": "github:dktr0/WebDirt"
}
},
"@strudel.cycles/xen": { "@strudel.cycles/xen": {
"version": "file:packages/xen", "version": "file:packages/xen",
"requires": { "requires": {
@ -18362,8 +18387,8 @@
"requires": { "requires": {
"@codemirror/lang-javascript": "^0.19.0", "@codemirror/lang-javascript": "^0.19.0",
"@strudel.cycles/core": "*", "@strudel.cycles/core": "*",
"@strudel.cycles/eval": "^0.1.0", "@strudel.cycles/eval": "^0.1.1",
"@strudel.cycles/tone": "^0.1.0", "@strudel.cycles/tone": "^0.1.1",
"@types/react": "^17.0.2", "@types/react": "^17.0.2",
"@types/react-dom": "^17.0.2", "@types/react-dom": "^17.0.2",
"@vitejs/plugin-react": "^1.3.0", "@vitejs/plugin-react": "^1.3.0",
@ -20004,6 +20029,10 @@
"defaults": "^1.0.3" "defaults": "^1.0.3"
} }
}, },
"WebDirt": {
"version": "git+ssh://git@github.com/dktr0/WebDirt.git#425dc8fd023440d9c61ffdb8642e44e2710faea0",
"from": "WebDirt@github:dktr0/WebDirt"
},
"webidl-conversions": { "webidl-conversions": {
"version": "6.1.0", "version": "6.1.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz",

View File

@ -6,7 +6,7 @@
"scripts": { "scripts": {
"test": "npm run test --workspaces --if-present && cd repl && npm run test", "test": "npm run test --workspaces --if-present && cd repl && npm run test",
"bootstrap": "lerna bootstrap", "bootstrap": "lerna bootstrap",
"setup": "npm i && npm run bootstrap && cd repl && npm i", "setup": "npm i && npm run bootstrap && cd repl && npm i && cd ../tutorial && npm i",
"repl": "cd repl && npm run dev", "repl": "cd repl && npm run dev",
"osc": "cd packages/osc && npm run server", "osc": "cd packages/osc && npm run server",
"build": "rm -rf out && cd repl && npm run build && cd ../tutorial && npm run build", "build": "rm -rf out && cd repl && npm run build && cd ../tutorial && npm run build",

View File

@ -760,4 +760,13 @@ generic_params.forEach(([type, name, description]) => {
Pattern.prototype[name] = _setter(controls[name]); Pattern.prototype[name] = _setter(controls[name]);
}); });
// create custom param
controls.createParam = (name) => {
Pattern.prototype[name] = _setter(controls[name]);
return (...pats) => _name(name, ...pats);
};
controls.createParams = (...names) =>
names.reduce((acc, name) => Object.assign(acc, { [name]: createParam(name) }), {});
export default controls; export default controls;

View File

@ -6,8 +6,8 @@
"packages": { "packages": {
"": { "": {
"name": "@strudel.cycles/core", "name": "@strudel.cycles/core",
"version": "0.0.3", "version": "0.1.0",
"license": "GPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"bjork": "^0.0.1", "bjork": "^0.0.1",
"fraction.js": "^4.2.0" "fraction.js": "^4.2.0"

View File

@ -1049,6 +1049,9 @@ export class Pattern {
.unit('c') .unit('c')
.slow(factor); .slow(factor);
} }
onTrigger(onTrigger) {
return this._withHap((hap) => hap.setContext({ ...hap.context, onTrigger }));
}
} }
// TODO - adopt value.mjs fully.. // TODO - adopt value.mjs fully..

View File

@ -15,9 +15,11 @@ npm i @strudel.cycles/eval --save
```js ```js
import { evaluate, extend } from '@strudel.cycles/eval'; import { evaluate, extend } from '@strudel.cycles/eval';
import * as strudel from '@strudel.cycles/core';
extend(strudel); // add strudel to eval scope evalScope(
import('@strudel.cycles/core'),
// import other strudel packages here
); // add strudel to eval scope
async function run(code) { async function run(code) {
const { pattern } = await evaluate(code); const { pattern } = await evaluate(code);

View File

@ -6,8 +6,8 @@
"packages": { "packages": {
"": { "": {
"name": "@strudel.cycles/eval", "name": "@strudel.cycles/eval",
"version": "0.0.3", "version": "0.1.1",
"license": "GPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"estraverse": "^5.3.0", "estraverse": "^5.3.0",
"shift-ast": "^6.1.0", "shift-ast": "^6.1.0",

View File

@ -6,8 +6,8 @@
"packages": { "packages": {
"": { "": {
"name": "@strudel.cycles/midi", "name": "@strudel.cycles/midi",
"version": "0.0.4", "version": "0.1.1",
"license": "GPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"tone": "^14.7.77", "tone": "^14.7.77",
"webmidi": "^2.5.2" "webmidi": "^2.5.2"

View File

@ -22,13 +22,15 @@ let startedAt = -1;
*/ */
Pattern.prototype.osc = function () { Pattern.prototype.osc = function () {
return this._withHap((hap) => { return this._withHap((hap) => {
const onTrigger = (time, hap, currentTime, cps, cycle, delta) => { const onTrigger = (time, hap, currentTime, cps) => {
const cycle = hap.wholeOrPart().begin.valueOf();
const delta = hap.duration.valueOf();
// time should be audio time of onset // time should be audio time of onset
// currentTime should be current time of audio context (slightly before time) // currentTime should be current time of audio context (slightly before time)
if (startedAt < 0) { if (startedAt < 0) {
startedAt = Date.now() - currentTime * 1000; startedAt = Date.now() - currentTime * 1000;
} }
const controls = Object.assign({}, { cps: cps, cycle: cycle, delta: delta }, hap.value); const controls = Object.assign({}, { cps, cycle, delta }, hap.value);
const keyvals = Object.entries(controls).flat(); const keyvals = Object.entries(controls).flat();
const ts = Math.floor(startedAt + (time + latency) * 1000); const ts = Math.floor(startedAt + (time + latency) * 1000);
const message = new OSC.Message('/dirt/play', ...keyvals); const message = new OSC.Message('/dirt/play', ...keyvals);

View File

@ -6,8 +6,8 @@
"packages": { "packages": {
"": { "": {
"name": "@strudel.cycles/osc", "name": "@strudel.cycles/osc",
"version": "0.0.1", "version": "0.1.0",
"license": "GPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"osc-js": "^2.3.2" "osc-js": "^2.3.2"
} }

File diff suppressed because one or more lines are too long

View File

@ -26,7 +26,7 @@ const ivory = '#abb2bf',
// background = '#292d3e', // background = '#292d3e',
background = 'transparent', background = 'transparent',
tooltipBackground = '#353a42', tooltipBackground = '#353a42',
selection = 'rgba(128, 203, 196, 0.2)', selection = 'rgba(128, 203, 196, 0.5)',
cursor = '#ffcc00'; cursor = '#ffcc00';
/// The editor theme styles for Material Palenight. /// The editor theme styles for Material Palenight.
@ -50,7 +50,8 @@ const materialPalenightTheme = EditorView.theme(
}, },
// done // done
'&.cm-focused .cm-cursor': { '&.cm-focused .cm-cursor': {
borderLeftColor: cursor, backgroundColor: cursor,
width: '3px',
}, },
'&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': { '&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': {
@ -80,7 +81,7 @@ const materialPalenightTheme = EditorView.theme(
// done // done
'.cm-gutters': { '.cm-gutters': {
background: '#2C323699', background: 'transparent',
color: '#676e95', color: '#676e95',
border: 'none', border: 'none',
}, },

3984
packages/react/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -57,14 +57,7 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawP
/* console.warn('no instrument chosen', event); /* console.warn('no instrument chosen', event);
throw new Error(`no instrument chosen for ${JSON.stringify(event)}`); */ throw new Error(`no instrument chosen for ${JSON.stringify(event)}`); */
} else { } else {
onTrigger( onTrigger(time, event, currentTime, 1 /* cps */);
time,
event,
currentTime,
1 /* cps */,
event.wholeOrPart().begin.valueOf(),
event.duration.valueOf(),
);
} }
} catch (err) { } catch (err) {
console.warn(err); console.warn(err);

View File

@ -16,7 +16,7 @@ const ivory = '#abb2bf',
// background = '#292d3e', // background = '#292d3e',
background = 'transparent', background = 'transparent',
tooltipBackground = '#353a42', tooltipBackground = '#353a42',
selection = 'rgba(128, 203, 196, 0.2)', selection = 'rgba(128, 203, 196, 0.5)',
cursor = '#ffcc00'; cursor = '#ffcc00';
/// The editor theme styles for Material Palenight. /// The editor theme styles for Material Palenight.
@ -40,7 +40,8 @@ export const materialPalenightTheme = EditorView.theme(
}, },
// done // done
'&.cm-focused .cm-cursor': { '&.cm-focused .cm-cursor': {
borderLeftColor: cursor, backgroundColor: cursor,
width: '3px',
}, },
'&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': { '&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': {
@ -70,7 +71,7 @@ export const materialPalenightTheme = EditorView.theme(
// done // done
'.cm-gutters': { '.cm-gutters': {
background: '#2C323699', background: 'transparent',
color: '#676e95', color: '#676e95',
border: 'none', border: 'none',
}, },

View File

@ -6,8 +6,8 @@
"packages": { "packages": {
"": { "": {
"name": "@strudel.cycles/tonal", "name": "@strudel.cycles/tonal",
"version": "0.0.3", "version": "0.1.1",
"license": "GPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"@tonaljs/tonal": "^4.6.5", "@tonaljs/tonal": "^4.6.5",
"webmidi": "^3.0.15" "webmidi": "^3.0.15"

View File

@ -20,7 +20,7 @@ export const getDrawContext = (id = 'test-canvas') => {
return canvas.getContext('2d'); return canvas.getContext('2d');
}; };
Pattern.prototype.draw = function (callback, cycleSpan, lookaheadCycles = 1) { Pattern.prototype.draw = function (callback, { from, to, onQuery }) {
if (window.strudelAnimation) { if (window.strudelAnimation) {
cancelAnimationFrame(window.strudelAnimation); cancelAnimationFrame(window.strudelAnimation);
} }
@ -29,19 +29,22 @@ Pattern.prototype.draw = function (callback, cycleSpan, lookaheadCycles = 1) {
events = []; events = [];
const animate = (time) => { const animate = (time) => {
const t = Tone.getTransport().seconds; const t = Tone.getTransport().seconds;
if (cycleSpan) { if (from !== undefined && to !== undefined) {
const currentCycle = Math.floor(t / cycleSpan); const currentCycle = Math.floor(t);
if (cycle !== currentCycle) { if (cycle !== currentCycle) {
cycle = currentCycle; cycle = currentCycle;
const begin = currentCycle * cycleSpan; const begin = currentCycle + from;
const end = (currentCycle + lookaheadCycles) * cycleSpan; const end = currentCycle + to;
events = this._asNumber(true) // true = silent error setTimeout(() => {
.query(new State(new TimeSpan(begin, end))) events = this._asNumber(true) // true = silent error
.filter(Boolean) .query(new State(new TimeSpan(begin, end)))
.filter((event) => event.part.begin.equals(event.whole.begin)); .filter(Boolean)
.filter((event) => event.part.begin.equals(event.whole.begin));
onQuery?.(events);
}, 0);
} }
} }
callback(ctx, events, t, cycleSpan, time); callback(ctx, events, t, time);
window.strudelAnimation = requestAnimationFrame(animate); window.strudelAnimation = requestAnimationFrame(animate);
}; };
requestAnimationFrame(animate); requestAnimationFrame(animate);

View File

@ -6,8 +6,8 @@
"packages": { "packages": {
"": { "": {
"name": "@strudel.cycles/tone", "name": "@strudel.cycles/tone",
"version": "0.0.4", "version": "0.1.1",
"license": "GPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"@tonejs/piano": "^0.2.1", "@tonejs/piano": "^0.2.1",
"chord-voicings": "^0.0.1", "chord-voicings": "^0.0.1",

View File

@ -6,39 +6,126 @@ This program is free software: you can redistribute it and/or modify it under th
import { Pattern } from '@strudel.cycles/core'; import { Pattern } from '@strudel.cycles/core';
const scale = (normalized, min, max) => normalized * (max - min) + min;
Pattern.prototype.pianoroll = function ({ Pattern.prototype.pianoroll = function ({
timeframe = 10, cycles = 4,
playhead = 0.5,
overscan = 1,
flipTime = 0,
flipValues = 0,
hideNegative = false,
inactive = '#C9E597', inactive = '#C9E597',
active = '#FFCA28', active = '#FFCA28',
background = '#2A3236', // background = '#2A3236',
background = 'transparent',
minMidi = 10,
maxMidi = 90, maxMidi = 90,
minMidi = 0, autorange = 0,
timeframe: timeframeProp,
fold = 0,
vertical = 0,
} = {}) { } = {}) {
const w = window.innerWidth; const ctx = getDrawContext();
const h = window.innerHeight; const w = ctx.canvas.width;
const midiRange = maxMidi - minMidi + 1; const h = ctx.canvas.height;
const height = h / midiRange; let from = -cycles * playhead;
let to = cycles * (1 - playhead);
if (timeframeProp) {
console.warn('timeframe is deprecated! use from/to instead');
from = 0;
to = timeframeProp;
}
if (!autorange && fold) {
console.warn('disabling autorange has no effect when fold is enabled');
}
const timeAxis = vertical ? h : w;
const valueAxis = vertical ? w : h;
let timeRange = vertical ? [timeAxis, 0] : [0, timeAxis]; // pixel range for time
const timeExtent = to - from; // number of seconds that fit inside the canvas frame
const valueRange = vertical ? [0, valueAxis] : [valueAxis, 0]; // pixel range for values
let valueExtent = maxMidi - minMidi + 1; // number of "slots" for values, overwritten if autorange true
let barThickness = valueAxis / valueExtent; // pixels per value, overwritten if autorange true
let foldValues = [];
flipTime && timeRange.reverse();
flipValues && valueRange.reverse();
const playheadPosition = scale(-from / timeExtent, ...timeRange);
this.draw( this.draw(
(ctx, events, t) => { (ctx, events, t) => {
ctx.fillStyle = background; ctx.fillStyle = background;
ctx.clearRect(0, 0, w, h); ctx.clearRect(0, 0, w, h);
ctx.fillRect(0, 0, w, h); ctx.fillRect(0, 0, w, h);
events.forEach((event) => { const inFrame = (event) =>
const isActive = event.whole.begin <= t && event.whole.end >= t; (!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.whole.end > t;
ctx.fillStyle = event.context?.color || inactive; ctx.fillStyle = event.context?.color || inactive;
ctx.strokeStyle = event.context?.color || active; ctx.strokeStyle = event.context?.color || active;
ctx.globalAlpha = event.context.velocity ?? 1; ctx.globalAlpha = event.context.velocity ?? 1;
const x = Math.round((event.whole.begin / timeframe) * w); ctx.beginPath();
const width = Math.round(((event.whole.end - event.whole.begin) / timeframe) * w); if (vertical) {
const y = Math.round(h - ((Number(event.value) - minMidi) / midiRange) * h); ctx.moveTo(0, playheadPosition);
const offset = (t / timeframe) * w; ctx.lineTo(valueAxis, playheadPosition);
const margin = 0; } else {
const coords = [x - offset + margin + 1, y + 1, width - 2, height - 2]; ctx.moveTo(playheadPosition, 0);
ctx.lineTo(playheadPosition, valueAxis);
}
ctx.stroke();
const timePx = scale((event.whole.begin - (flipTime ? to : from)) / timeExtent, ...timeRange);
let durationPx = scale(event.duration / timeExtent, 0, timeAxis);
const valuePx = scale(
fold ? foldValues.indexOf(event.value) / foldValues.length : (Number(event.value) - minMidi) / valueExtent,
...valueRange,
);
let margin = 0;
const offset = scale(t / timeExtent, ...timeRange);
let coords;
if (vertical) {
coords = [
valuePx + 1 - (flipValues ? barThickness : 0), // x
timeAxis - offset + timePx + margin + 1 - (flipTime ? 0 : durationPx), // y
barThickness - 2, // width
durationPx - 2, // height
];
} else {
coords = [
timePx - offset + margin + 1 - (flipTime ? durationPx : 0), // x
valuePx + 1 - (flipValues ? 0 : barThickness), // y
durationPx - 2, // widith
barThickness - 2, // height
];
}
isActive ? ctx.strokeRect(...coords) : ctx.fillRect(...coords); isActive ? ctx.strokeRect(...coords) : ctx.fillRect(...coords);
}); });
}, },
timeframe, {
2, // lookaheadCycles from: from - overscan,
to: to + overscan,
onQuery: (events) => {
const getValue = (e) => Number(e.value);
const { min, max, values } = events.reduce(
({ min, max, values }, e) => {
const v = getValue(e);
return {
min: v < min ? v : min,
max: v > max ? v : max,
values: values.includes(v) ? values : [...values, v],
};
},
{ min: Infinity, max: -Infinity, values: [] },
);
if (autorange) {
minMidi = min;
maxMidi = max;
valueExtent = maxMidi - minMidi + 1;
}
foldValues = values.sort((a, b) => a - b);
barThickness = fold ? valueAxis / foldValues.length : valueAxis / valueExtent;
},
},
); );
return this; return this;
}; };

View File

@ -10,18 +10,20 @@ import { State, TimeSpan } from '@strudel.cycles/core';
export class Scheduler { export class Scheduler {
worker; worker;
pattern; pattern;
constructor({ audioContext, interval = 0.2, onEvent }) { constructor({ audioContext, interval = 0.2, onEvent, latency = 0.2 }) {
this.worker = new ClockWorker( this.worker = new ClockWorker(
audioContext, audioContext,
(begin, end) => { (begin, end) => {
this.pattern.query(new State(new TimeSpan(begin, end))).forEach((e) => { this.pattern.query(new State(new TimeSpan(begin + latency, end + latency))).forEach((e) => {
if (!e.part.begin.equals(e.whole.begin)) { if (!e.part.begin.equals(e.whole.begin)) {
return; return;
} }
if (e.context.onTrigger) {
// TODO: kill first param, as it's contained in e
e.context.onTrigger(e.whole.begin, e, audioContext.currentTime, 1 /* cps */);
}
if (onEvent) { if (onEvent) {
onEvent?.(e); onEvent?.(e);
} else {
console.warn('unplayable event: no audio node nor onEvent callback', e);
} }
}); });
}, },

View File

@ -0,0 +1,11 @@
# @strudel.cycles/webdirt
This package adds [webdirt](https://github.com/dktr0/WebDirt) support to strudel!
## Dev Notes
Add default samples to repl:
1. move samples to `repl/public` folder. the samples folder is expected to have subfolders, with samples in it. the subfolders will be the names of the samples.
2. run `./makeSampleMap.sh ../../repl/public/EmuSP12 > ../../repl/public/EmuSP12.json`
3. adapt `loadWebDirt` in App.jsx + MiniRepl.jsx to use the generated json file

View File

@ -0,0 +1,2 @@
export * from './webdirt.mjs';
export * from './sampler.mjs';

View File

@ -0,0 +1,32 @@
#/bin/sh
printf "{\n"
dircount=0
# for d in $searchRoot/*; do
find $1 -mindepth 1 -maxdepth 1 -iname "*" | sort | while read d; do
if [ -d "$d" ]
then
if [ $dircount -ne 0 ]
then
printf ",\n"
fi
(( dircount++ ))
dirname=`basename $d`
printf "\"%s\": [" "$dirname"
search2=$searchRoot/$dirname/*.WAV
filecount=0
find "$d" -iname "*.wav" | sort | while read f; do
# for f in $search2; do
filename=$(printf %q "$f")
basename=${f##*/}
if [[ ${basename:0:1} != "." ]]; then
if [ $filecount -ne 0 ]; then
printf ","
fi
(( filecount++ ))
printf "\"%s/%s\"" "$dirname" "$basename"
fi
done
printf "]"
fi
done
printf "\n}\n"

28
packages/webdirt/package-lock.json generated Normal file
View File

@ -0,0 +1,28 @@
{
"name": "@strudel.cycles/webdirt",
"version": "0.1.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@strudel.cycles/webdirt",
"version": "0.1.0",
"license": "AGPL-3.0-or-later",
"dependencies": {
"WebDirt": "github:dktr0/WebDirt"
}
},
"node_modules/WebDirt": {
"name": "webdirt",
"version": "1.0.0",
"resolved": "git+ssh://git@github.com/dktr0/WebDirt.git#425dc8fd023440d9c61ffdb8642e44e2710faea0",
"license": "ISC"
}
},
"dependencies": {
"WebDirt": {
"version": "git+ssh://git@github.com/dktr0/WebDirt.git#425dc8fd023440d9c61ffdb8642e44e2710faea0",
"from": "WebDirt@github:dktr0/WebDirt"
}
}
}

View File

@ -0,0 +1,28 @@
{
"name": "@strudel.cycles/webdirt",
"version": "0.1.0",
"description": "WebDirt integration for Strudel",
"main": "index.mjs",
"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>",
"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": "^0.1.0",
"WebDirt": "github:dktr0/WebDirt"
}
}

View File

@ -0,0 +1,112 @@
const bufferCache = {}; // string: Promise<ArrayBuffer>
const loadCache = {}; // string: Promise<ArrayBuffer>
export const loadBuffer = (url, ac) => {
if (!loadCache[url]) {
loadCache[url] = fetch(url)
.then((res) => res.arrayBuffer())
.then(async (res) => {
const decoded = await ac.decodeAudioData(res);
bufferCache[url] = decoded;
return decoded;
});
}
return loadCache[url];
};
export const getLoadedBuffer = (url) => {
return bufferCache[url];
};
/* export const playBuffer = (buffer, time = ac.currentTime, destination = ac.destination) => {
const src = ac.createBufferSource();
src.buffer = buffer;
src.connect(destination);
src.start(time);
};
export const playSample = async (url) => playBuffer(await loadBuffer(url)); */
// https://estuary.mcmaster.ca/samples/resources.json
// Array<{ "url":string, "bank": string, "n": number}>
// ritchse/tidal-drum-machines/tree/main/machines/AkaiLinn
const githubCache = {};
let sampleCache = { current: undefined };
export const loadGithubSamples = async (path, nameFn) => {
const storageKey = 'loadGithubSamples ' + path;
const stored = localStorage.getItem(storageKey);
if (stored) {
console.log('[sampler]: loaded sample list from localstorage', path);
githubCache[path] = JSON.parse(stored);
}
if (githubCache[path]) {
sampleCache.current = githubCache[path];
return githubCache[path];
}
console.log('[sampler]: fetching sample list from github', path);
try {
const [user, repo, ...folders] = path.split('/');
const baseUrl = `https://api.github.com/repos/${user}/${repo}/contents`;
const banks = await fetch(`${baseUrl}/${folders.join('/')}`).then((res) => res.json());
// fetch each subfolder
githubCache[path] = (
await Promise.all(
banks.map(async ({ name, path }) => ({
name,
content: await fetch(`${baseUrl}/${path}`)
.then((res) => res.json())
.catch((err) => {
console.error('could not load path', err);
}),
})),
)
)
.filter(({ content }) => !!content)
.reduce(
(acc, { name, content }) => ({
...acc,
[nameFn?.(name) || name]: content.map(({ download_url }) => download_url),
}),
{},
);
} catch (err) {
console.error('[sampler]: failed to fetch sample list from github', err);
return;
}
sampleCache.current = githubCache[path];
localStorage.setItem(storageKey, JSON.stringify(sampleCache.current));
console.log('[sampler]: loaded samples:', sampleCache.current);
return githubCache[path];
};
/**
* load the given sample map for webdirt
*
* @example
* loadSamples({
* bd: '808bd/BD0000.WAV',
* sd: ['808sd/SD0000.WAV','808sd/SD0010.WAV','808sd/SD0050.WAV']
* }, 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/');
* s("bd <sd!7 sd(3,4,2)>").n(2).webdirt()
*
*/
export const samples = (sampleMap, baseUrl = '') => {
sampleCache.current = {
...sampleCache.current,
...Object.fromEntries(
Object.entries(sampleMap).map(([key, value]) => [
key,
(typeof value === 'string' ? [value] : value).map((v) =>
(baseUrl + v).replace('github:', 'https://raw.githubusercontent.com/'),
),
]),
),
};
};
export const resetLoadedSamples = () => {
sampleCache.current = undefined;
};
export const getLoadedSamples = () => sampleCache.current;

View File

@ -0,0 +1,98 @@
import * as strudel from '@strudel.cycles/core';
const { Pattern } = strudel;
import * as WebDirt from 'WebDirt';
import { getLoadedSamples, loadBuffer, getLoadedBuffer } from './sampler.mjs';
let webDirt;
/*
example config:
{
sampleMapUrl: 'EmuSP12.json',
sampleFolder: 'EmuSP12',
}
*/
export function loadWebDirt(config) {
webDirt = new WebDirt.WebDirt(config);
webDirt.initializeWebAudio();
}
/**
*
* Uses [webdirt](https://github.com/dktr0/WebDirt) as output.
*
* <details>
* <summary>show supported Webdirt controls</summary>
*
* - s :: String, -- name of sample bank
* - n :: Int, -- number of sample within a bank
* - {@link gain} :: Number, -- clamped from 0 to 2; 1 is default and full-scale
* - overgain :: Number, -- additional gain added to gain to go past clamp at 2
* - {@link pan} :: Number, -- range: 0 to 1
* - nudge :: Number, -- nudge the time of the sample forwards/backwards in seconds
* - {@link speed} :: Number, -- speed / pitch of the sample
* - {@link unit} :: String
* - note :: Number, -- pitch offset in semitones
* - {@link begin} :: Number, -- cut from sample start, normalized
* - {@link end} :: Number, -- cut from sample end, normalized
* - {@link cut} :: Int, -- samples with same cut number will interupt each other
* - {@link cutoff} :: Number, -- lowpass filter frequency
* - {@link resonance} :: Number, -- lowpass filter resonance
* - {@link hcutoff} :: Number, -- highpass filter frequency
* - {@link hresonance} :: Number, -- highpass filter resonance
* - {@link bandf} :: Number, -- bandpass filter frequency
* - {@link bandq} :: Number, -- bandpass filter resonance
* - {@link vowel} :: String, -- name of vowel ('a' | 'e' | 'i' | 'o' | 'u')
* - delay :: Number, -- delay wet/dry mix
* - delaytime :: Number, -- delay time in seconds
* - delayfeedback :: Number, -- delay feedback
* - {@link loop} :: Number, -- loop sample n times (relative to sample length)
* - {@link crush} :: Number, -- bitcrusher (currently not working)
* - {@link coarse} :: Number, -- coarse effect (currently not working)
* - {@link shape} :: Number, -- (currently not working)
*
* </details>
*
* @name webdirt
* @memberof Pattern
* @returns Pattern
* @example
* s("bd*2 hh sd hh").n("<0 1>").webdirt()
*/
Pattern.prototype.webdirt = function () {
// create a WebDirt object and initialize Web Audio context
return this._withHap((hap) => {
const onTrigger = async (time, e, currentTime) => {
if (!webDirt) {
throw new Error('WebDirt not initialized!');
}
const deadline = time - currentTime;
const { s, n = 0, ...rest } = e.value || {};
if (!s) {
console.warn('Pattern.webdirt: no "s" was set!');
}
const samples = getLoadedSamples();
if (!samples?.[s]) {
// try default samples
webDirt.playSample({ s, n, ...rest }, deadline);
return;
}
if (!samples?.[s]) {
console.warn(`Pattern.webdirt: sample "${s}" not found in loaded samples`, samples);
} else {
const bank = samples[s];
const sampleUrl = bank[n % bank.length];
const buffer = getLoadedBuffer(sampleUrl);
if (!buffer) {
console.log(`Pattern.webdirt: load ${s}:${n} from ${sampleUrl}`);
loadBuffer(sampleUrl, webDirt.ac);
} else {
const msg = { buffer: { buffer }, ...rest };
webDirt.playSample(msg, deadline);
}
}
};
return hap.setContext({ ...hap.context, onTrigger });
});
};

16
repl/public/EmuSP12.json Normal file
View File

@ -0,0 +1,16 @@
{
"bd": ["bd/Bassdrum-01.wav","bd/Bassdrum-02.wav","bd/Bassdrum-03.wav","bd/Bassdrum-04.wav","bd/Bassdrum-05.wav","bd/Bassdrum-06.wav","bd/Bassdrum-07.wav","bd/Bassdrum-08.wav","bd/Bassdrum-09.wav","bd/Bassdrum-10.wav","bd/Bassdrum-11.wav","bd/Bassdrum-12.wav","bd/Bassdrum-13.wav","bd/Bassdrum-14.wav"],
"cb": ["cb/Cowbell.wav"],
"cp": ["cp/Clap.wav"],
"cr": ["cr/Crash.wav"],
"hh": ["hh/Hat Closed-01.wav","hh/Hat Closed-02.wav"],
"ht": ["ht/Tom H-01.wav","ht/Tom H-02.wav","ht/Tom H-03.wav","ht/Tom H-04.wav","ht/Tom H-05.wav","ht/Tom H-06.wav"],
"lt": ["lt/Tom L-01.wav","lt/Tom L-02.wav","lt/Tom L-03.wav","lt/Tom L-04.wav","lt/Tom L-05.wav","lt/Tom L-06.wav"],
"misc": ["misc/Metal-01.wav","misc/Metal-02.wav","misc/Metal-03.wav","misc/Scratch.wav","misc/Shot-01.wav","misc/Shot-02.wav","misc/Shot-03.wav"],
"mt": ["mt/Tom M-01.wav","mt/Tom M-02.wav","mt/Tom M-03.wav","mt/Tom M-05.wav"],
"oh": ["oh/Hhopen1.wav"],
"perc": ["perc/Blow1.wav"],
"rd": ["rd/Ride.wav"],
"rim": ["rim/zRim Shot-01.wav","rim/zRim Shot-02.wav"],
"sd": ["sd/Snaredrum-01.wav","sd/Snaredrum-02.wav","sd/Snaredrum-03.wav","sd/Snaredrum-04.wav","sd/Snaredrum-05.wav","sd/Snaredrum-06.wav","sd/Snaredrum-07.wav","sd/Snaredrum-08.wav","sd/Snaredrum-09.wav","sd/Snaredrum-10.wav","sd/Snaredrum-11.wav","sd/Snaredrum-12.wav","sd/Snaredrum-13.wav","sd/Snaredrum-14.wav","sd/Snaredrum-15.wav","sd/Snaredrum-16.wav","sd/Snaredrum-17.wav","sd/Snaredrum-18.wav","sd/Snaredrum-19.wav","sd/Snaredrum-20.wav","sd/Snaredrum-21.wav"]
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

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