mirror of
https://github.com/eliasstepanik/strudel-docker.git
synced 2026-01-21 02:28:34 +00:00
Merge pull request #255 from tidalcycles/repl-refactoring
Repl refactoring
This commit is contained in:
commit
4bc2f64b24
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"source": {
|
"source": {
|
||||||
"includePattern": ".+\\.(js(doc|x)?|mjs)$",
|
"includePattern": ".+\\.(js(doc|x)?|mjs)$",
|
||||||
"excludePattern": "node_modules|shift-parser|shift-reducer|shift-traverser"
|
"excludePattern": "node_modules|shift-parser|shift-reducer|shift-traverser|dist"
|
||||||
},
|
},
|
||||||
"plugins": ["plugins/markdown"],
|
"plugins": ["plugins/markdown"],
|
||||||
"opts": {
|
"opts": {
|
||||||
|
|||||||
41
package-lock.json
generated
41
package-lock.json
generated
@ -12495,10 +12495,10 @@
|
|||||||
},
|
},
|
||||||
"packages/midi": {
|
"packages/midi": {
|
||||||
"name": "@strudel.cycles/midi",
|
"name": "@strudel.cycles/midi",
|
||||||
"version": "0.3.2",
|
"version": "0.3.3",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@strudel.cycles/tone": "^0.3.2",
|
"@strudel.cycles/tone": "^0.3.3",
|
||||||
"tone": "^14.7.77",
|
"tone": "^14.7.77",
|
||||||
"webmidi": "^3.0.21"
|
"webmidi": "^3.0.21"
|
||||||
}
|
}
|
||||||
@ -12519,12 +12519,12 @@
|
|||||||
},
|
},
|
||||||
"packages/mini": {
|
"packages/mini": {
|
||||||
"name": "@strudel.cycles/mini",
|
"name": "@strudel.cycles/mini",
|
||||||
"version": "0.3.2",
|
"version": "0.3.3",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@strudel.cycles/core": "^0.3.2",
|
"@strudel.cycles/core": "^0.3.2",
|
||||||
"@strudel.cycles/eval": "^0.3.2",
|
"@strudel.cycles/eval": "^0.3.2",
|
||||||
"@strudel.cycles/tone": "^0.3.2"
|
"@strudel.cycles/tone": "^0.3.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"peggy": "^2.0.1"
|
"peggy": "^2.0.1"
|
||||||
@ -12540,13 +12540,14 @@
|
|||||||
},
|
},
|
||||||
"packages/react": {
|
"packages/react": {
|
||||||
"name": "@strudel.cycles/react",
|
"name": "@strudel.cycles/react",
|
||||||
"version": "0.3.2",
|
"version": "0.3.3",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/lang-javascript": "^6.1.1",
|
"@codemirror/lang-javascript": "^6.1.1",
|
||||||
"@strudel.cycles/core": "^0.3.2",
|
"@strudel.cycles/core": "^0.3.2",
|
||||||
"@strudel.cycles/eval": "^0.3.2",
|
"@strudel.cycles/tone": "^0.3.3",
|
||||||
"@strudel.cycles/tone": "^0.3.2",
|
"@strudel.cycles/transpiler": "^0.3.2",
|
||||||
|
"@strudel.cycles/webaudio": "^0.3.3",
|
||||||
"@uiw/codemirror-themes": "^4.12.4",
|
"@uiw/codemirror-themes": "^4.12.4",
|
||||||
"@uiw/react-codemirror": "^4.12.4",
|
"@uiw/react-codemirror": "^4.12.4",
|
||||||
"react-hook-inview": "^4.5.0"
|
"react-hook-inview": "^4.5.0"
|
||||||
@ -12621,11 +12622,11 @@
|
|||||||
},
|
},
|
||||||
"packages/soundfonts": {
|
"packages/soundfonts": {
|
||||||
"name": "@strudel.cycles/soundfonts",
|
"name": "@strudel.cycles/soundfonts",
|
||||||
"version": "0.3.2",
|
"version": "0.3.3",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@strudel.cycles/core": "^0.3.2",
|
"@strudel.cycles/core": "^0.3.2",
|
||||||
"@strudel.cycles/webaudio": "^0.3.2",
|
"@strudel.cycles/webaudio": "^0.3.3",
|
||||||
"sfumato": "^0.1.2",
|
"sfumato": "^0.1.2",
|
||||||
"soundfont2": "^0.4.0"
|
"soundfont2": "^0.4.0"
|
||||||
},
|
},
|
||||||
@ -12653,7 +12654,7 @@
|
|||||||
},
|
},
|
||||||
"packages/tonal": {
|
"packages/tonal": {
|
||||||
"name": "@strudel.cycles/tonal",
|
"name": "@strudel.cycles/tonal",
|
||||||
"version": "0.3.2",
|
"version": "0.3.3",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@strudel.cycles/core": "^0.3.2",
|
"@strudel.cycles/core": "^0.3.2",
|
||||||
@ -12678,7 +12679,7 @@
|
|||||||
},
|
},
|
||||||
"packages/tone": {
|
"packages/tone": {
|
||||||
"name": "@strudel.cycles/tone",
|
"name": "@strudel.cycles/tone",
|
||||||
"version": "0.3.2",
|
"version": "0.3.3",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@strudel.cycles/core": "^0.3.2",
|
"@strudel.cycles/core": "^0.3.2",
|
||||||
@ -12709,7 +12710,7 @@
|
|||||||
},
|
},
|
||||||
"packages/webaudio": {
|
"packages/webaudio": {
|
||||||
"name": "@strudel.cycles/webaudio",
|
"name": "@strudel.cycles/webaudio",
|
||||||
"version": "0.3.2",
|
"version": "0.3.3",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@strudel.cycles/core": "^0.3.2"
|
"@strudel.cycles/core": "^0.3.2"
|
||||||
@ -14427,7 +14428,7 @@
|
|||||||
"@strudel.cycles/midi": {
|
"@strudel.cycles/midi": {
|
||||||
"version": "file:packages/midi",
|
"version": "file:packages/midi",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@strudel.cycles/tone": "^0.3.2",
|
"@strudel.cycles/tone": "^0.3.3",
|
||||||
"tone": "^14.7.77",
|
"tone": "^14.7.77",
|
||||||
"webmidi": "^3.0.21"
|
"webmidi": "^3.0.21"
|
||||||
},
|
},
|
||||||
@ -14448,7 +14449,7 @@
|
|||||||
"requires": {
|
"requires": {
|
||||||
"@strudel.cycles/core": "^0.3.2",
|
"@strudel.cycles/core": "^0.3.2",
|
||||||
"@strudel.cycles/eval": "^0.3.2",
|
"@strudel.cycles/eval": "^0.3.2",
|
||||||
"@strudel.cycles/tone": "^0.3.2",
|
"@strudel.cycles/tone": "^0.3.3",
|
||||||
"peggy": "^2.0.1"
|
"peggy": "^2.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -14463,8 +14464,9 @@
|
|||||||
"requires": {
|
"requires": {
|
||||||
"@codemirror/lang-javascript": "^6.1.1",
|
"@codemirror/lang-javascript": "^6.1.1",
|
||||||
"@strudel.cycles/core": "^0.3.2",
|
"@strudel.cycles/core": "^0.3.2",
|
||||||
"@strudel.cycles/eval": "^0.3.2",
|
"@strudel.cycles/tone": "^0.3.3",
|
||||||
"@strudel.cycles/tone": "^0.3.2",
|
"@strudel.cycles/transpiler": "^0.3.2",
|
||||||
|
"@strudel.cycles/webaudio": "^0.3.3",
|
||||||
"@types/react": "^17.0.2",
|
"@types/react": "^17.0.2",
|
||||||
"@types/react-dom": "^17.0.2",
|
"@types/react-dom": "^17.0.2",
|
||||||
"@uiw/codemirror-themes": "^4.12.4",
|
"@uiw/codemirror-themes": "^4.12.4",
|
||||||
@ -14520,7 +14522,7 @@
|
|||||||
"version": "file:packages/soundfonts",
|
"version": "file:packages/soundfonts",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@strudel.cycles/core": "^0.3.2",
|
"@strudel.cycles/core": "^0.3.2",
|
||||||
"@strudel.cycles/webaudio": "^0.3.2",
|
"@strudel.cycles/webaudio": "^0.3.3",
|
||||||
"node-fetch": "^3.2.6",
|
"node-fetch": "^3.2.6",
|
||||||
"sfumato": "^0.1.2",
|
"sfumato": "^0.1.2",
|
||||||
"soundfont2": "^0.4.0"
|
"soundfont2": "^0.4.0"
|
||||||
@ -20343,8 +20345,9 @@
|
|||||||
"requires": {
|
"requires": {
|
||||||
"@codemirror/lang-javascript": "^6.1.1",
|
"@codemirror/lang-javascript": "^6.1.1",
|
||||||
"@strudel.cycles/core": "^0.3.2",
|
"@strudel.cycles/core": "^0.3.2",
|
||||||
"@strudel.cycles/eval": "^0.3.2",
|
"@strudel.cycles/tone": "^0.3.3",
|
||||||
"@strudel.cycles/tone": "^0.3.2",
|
"@strudel.cycles/transpiler": "^0.3.2",
|
||||||
|
"@strudel.cycles/webaudio": "^0.3.3",
|
||||||
"@types/react": "^17.0.2",
|
"@types/react": "^17.0.2",
|
||||||
"@types/react-dom": "^17.0.2",
|
"@types/react-dom": "^17.0.2",
|
||||||
"@uiw/codemirror-themes": "^4.12.4",
|
"@uiw/codemirror-themes": "^4.12.4",
|
||||||
|
|||||||
@ -5,6 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import createClock from './zyklus.mjs';
|
import createClock from './zyklus.mjs';
|
||||||
|
import { logger } from './logger.mjs';
|
||||||
|
|
||||||
export class Cyclist {
|
export class Cyclist {
|
||||||
worker;
|
worker;
|
||||||
@ -13,8 +14,10 @@ export class Cyclist {
|
|||||||
cps = 1; // TODO
|
cps = 1; // TODO
|
||||||
getTime;
|
getTime;
|
||||||
phase = 0;
|
phase = 0;
|
||||||
constructor({ interval, onTrigger, onError, getTime, latency = 0.1 }) {
|
constructor({ interval, onTrigger, onToggle, onError, getTime, latency = 0.1 }) {
|
||||||
this.getTime = getTime;
|
this.getTime = getTime;
|
||||||
|
this.onToggle = onToggle;
|
||||||
|
this.latency = latency;
|
||||||
const round = (x) => Math.round(x * 1000) / 1000;
|
const round = (x) => Math.round(x * 1000) / 1000;
|
||||||
this.clock = createClock(
|
this.clock = createClock(
|
||||||
getTime,
|
getTime,
|
||||||
@ -28,9 +31,7 @@ export class Cyclist {
|
|||||||
const time = getTime();
|
const time = getTime();
|
||||||
try {
|
try {
|
||||||
const haps = this.pattern.queryArc(begin, end); // get Haps
|
const haps = this.pattern.queryArc(begin, end); // get Haps
|
||||||
// console.log('haps', haps.map((hap) => hap.value.n).join(' '));
|
|
||||||
haps.forEach((hap) => {
|
haps.forEach((hap) => {
|
||||||
// console.log('hap', hap.value.n, hap.part.begin);
|
|
||||||
if (hap.part.begin.equals(hap.whole.begin)) {
|
if (hap.part.begin.equals(hap.whole.begin)) {
|
||||||
const deadline = hap.whole.begin + this.origin - time + latency;
|
const deadline = hap.whole.begin + this.origin - time + latency;
|
||||||
const duration = hap.duration * 1;
|
const duration = hap.duration * 1;
|
||||||
@ -38,7 +39,7 @@ export class Cyclist {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('scheduler error', e);
|
logger(`[cyclist] error: ${e.message}`);
|
||||||
onError?.(e);
|
onError?.(e);
|
||||||
}
|
}
|
||||||
}, // called slightly before each cycle
|
}, // called slightly before each cycle
|
||||||
@ -46,24 +47,29 @@ export class Cyclist {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
getPhase() {
|
getPhase() {
|
||||||
return this.phase;
|
return this.getTime() - this.origin - this.latency;
|
||||||
|
}
|
||||||
|
setStarted(v) {
|
||||||
|
this.started = v;
|
||||||
|
this.onToggle?.(v);
|
||||||
}
|
}
|
||||||
start() {
|
start() {
|
||||||
if (!this.pattern) {
|
if (!this.pattern) {
|
||||||
throw new Error('Scheduler: no pattern set! call .setPattern first.');
|
throw new Error('Scheduler: no pattern set! call .setPattern first.');
|
||||||
}
|
}
|
||||||
|
logger('[cyclist] start');
|
||||||
this.clock.start();
|
this.clock.start();
|
||||||
this.started = true;
|
this.setStarted(true);
|
||||||
}
|
}
|
||||||
pause() {
|
pause() {
|
||||||
this.clock.stop();
|
logger('[cyclist] pause');
|
||||||
delete this.origin;
|
this.clock.pause();
|
||||||
this.started = false;
|
this.setStarted(false);
|
||||||
}
|
}
|
||||||
stop() {
|
stop() {
|
||||||
delete this.origin;
|
logger('[cyclist] stop');
|
||||||
this.clock.stop();
|
this.clock.stop();
|
||||||
this.started = false;
|
this.setStarted(false);
|
||||||
}
|
}
|
||||||
setPattern(pat, autostart = false) {
|
setPattern(pat, autostart = false) {
|
||||||
this.pattern = pat;
|
this.pattern = pat;
|
||||||
|
|||||||
@ -4,8 +4,7 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
|
|||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Tone } from './tone.mjs';
|
import { Pattern, getTime } from './index.mjs';
|
||||||
import { Pattern } from '@strudel.cycles/core';
|
|
||||||
|
|
||||||
export const getDrawContext = (id = 'test-canvas') => {
|
export const getDrawContext = (id = 'test-canvas') => {
|
||||||
let canvas = document.querySelector('#' + id);
|
let canvas = document.querySelector('#' + id);
|
||||||
@ -28,7 +27,7 @@ Pattern.prototype.draw = function (callback, { from, to, onQuery }) {
|
|||||||
let cycle,
|
let cycle,
|
||||||
events = [];
|
events = [];
|
||||||
const animate = (time) => {
|
const animate = (time) => {
|
||||||
const t = Tone.getTransport().seconds;
|
const t = getTime();
|
||||||
if (from !== undefined && to !== undefined) {
|
if (from !== undefined && to !== undefined) {
|
||||||
const currentCycle = Math.floor(t);
|
const currentCycle = Math.floor(t);
|
||||||
if (cycle !== currentCycle) {
|
if (cycle !== currentCycle) {
|
||||||
@ -50,9 +49,9 @@ Pattern.prototype.draw = function (callback, { from, to, onQuery }) {
|
|||||||
return this;
|
return this;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const cleanupDraw = () => {
|
export const cleanupDraw = (clearScreen = true) => {
|
||||||
const ctx = getDrawContext();
|
const ctx = getDrawContext();
|
||||||
ctx.clearRect(0, 0, window.innerWidth, window.innerHeight);
|
clearScreen && ctx.clearRect(0, 0, window.innerWidth, window.innerHeight);
|
||||||
if (window.strudelAnimation) {
|
if (window.strudelAnimation) {
|
||||||
cancelAnimationFrame(window.strudelAnimation);
|
cancelAnimationFrame(window.strudelAnimation);
|
||||||
}
|
}
|
||||||
@ -4,9 +4,7 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
|
|||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as strudel from '@strudel.cycles/core';
|
import { isPattern, Pattern } from './index.mjs';
|
||||||
|
|
||||||
const { isPattern, Pattern } = strudel;
|
|
||||||
|
|
||||||
let scoped = false;
|
let scoped = false;
|
||||||
export const evalScope = async (...args) => {
|
export const evalScope = async (...args) => {
|
||||||
|
|||||||
@ -55,7 +55,8 @@ Fraction.prototype.min = function (other) {
|
|||||||
return this.lt(other) ? this : other;
|
return this.lt(other) ? this : other;
|
||||||
};
|
};
|
||||||
|
|
||||||
Fraction.prototype.show = function () {
|
Fraction.prototype.show = function (/* excludeWhole = false */) {
|
||||||
|
// return this.toFraction(excludeWhole);
|
||||||
return this.s * this.n + '/' + this.d;
|
return this.s * this.n + '/' + this.d;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -85,9 +85,13 @@ export class Hap {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
showWhole() {
|
showWhole(compact = false) {
|
||||||
return `${this.whole == undefined ? '~' : this.whole.show()}: ${
|
return `${this.whole == undefined ? '~' : this.whole.show()}: ${
|
||||||
typeof this.value === 'object' ? JSON.stringify(this.value) : this.value
|
typeof this.value === 'object'
|
||||||
|
? compact
|
||||||
|
? JSON.stringify(this.value).slice(1, -1).replaceAll('"', '').replaceAll(',', ' ')
|
||||||
|
: JSON.stringify(this.value)
|
||||||
|
: this.value
|
||||||
}`;
|
}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -7,6 +7,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
import controls from './controls.mjs';
|
import controls from './controls.mjs';
|
||||||
export * from './euclid.mjs';
|
export * from './euclid.mjs';
|
||||||
import Fraction from './fraction.mjs';
|
import Fraction from './fraction.mjs';
|
||||||
|
import { logger } from './logger.mjs';
|
||||||
export { Fraction, controls };
|
export { Fraction, controls };
|
||||||
export * from './hap.mjs';
|
export * from './hap.mjs';
|
||||||
export * from './pattern.mjs';
|
export * from './pattern.mjs';
|
||||||
@ -17,15 +18,17 @@ export * from './util.mjs';
|
|||||||
export * from './speak.mjs';
|
export * from './speak.mjs';
|
||||||
export * from './evaluate.mjs';
|
export * from './evaluate.mjs';
|
||||||
export * from './repl.mjs';
|
export * from './repl.mjs';
|
||||||
|
export * from './logger.mjs';
|
||||||
|
export * from './time.mjs';
|
||||||
|
export * from './draw.mjs';
|
||||||
|
export * from './pianoroll.mjs';
|
||||||
|
export * from './ui.mjs';
|
||||||
export { default as drawLine } from './drawLine.mjs';
|
export { default as drawLine } from './drawLine.mjs';
|
||||||
export { default as gist } from './gist.js';
|
export { default as gist } from './gist.js';
|
||||||
// below won't work with runtime.mjs (json import fails)
|
// below won't work with runtime.mjs (json import fails)
|
||||||
/* import * as p from './package.json';
|
/* import * as p from './package.json';
|
||||||
export const version = p.version; */
|
export const version = p.version; */
|
||||||
console.log(
|
logger('🌀 @strudel.cycles/core loaded 🌀');
|
||||||
'%c // 🌀 @strudel.cycles/core loaded 🌀', // keep "//" for runnable snapshot source..
|
|
||||||
'background-color: black;color:white;padding:4px;border-radius:15px',
|
|
||||||
);
|
|
||||||
if (globalThis._strudelLoaded) {
|
if (globalThis._strudelLoaded) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`@strudel.cycles/core was loaded more than once...
|
`@strudel.cycles/core was loaded more than once...
|
||||||
|
|||||||
18
packages/core/logger.mjs
Normal file
18
packages/core/logger.mjs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
export const logKey = 'strudel.log';
|
||||||
|
|
||||||
|
export function logger(message, type, data = {}) {
|
||||||
|
console.log(`%c${message}`, 'background-color: black;color:white;border-radius:15px');
|
||||||
|
if (typeof CustomEvent !== 'undefined') {
|
||||||
|
document.dispatchEvent(
|
||||||
|
new CustomEvent(logKey, {
|
||||||
|
detail: {
|
||||||
|
message,
|
||||||
|
type,
|
||||||
|
data,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.key = logKey;
|
||||||
@ -12,6 +12,7 @@ import { unionWithObj } from './value.mjs';
|
|||||||
|
|
||||||
import { compose, removeUndefineds, flatten, id, listRange, curry, mod, numeralArgs, parseNumeral } from './util.mjs';
|
import { compose, removeUndefineds, flatten, id, listRange, curry, mod, numeralArgs, parseNumeral } from './util.mjs';
|
||||||
import drawLine from './drawLine.mjs';
|
import drawLine from './drawLine.mjs';
|
||||||
|
import { logger } from './logger.mjs';
|
||||||
|
|
||||||
let stringParser;
|
let stringParser;
|
||||||
// parser is expected to turn a string into a pattern
|
// parser is expected to turn a string into a pattern
|
||||||
@ -1328,22 +1329,25 @@ export class Pattern {
|
|||||||
.unit('c')
|
.unit('c')
|
||||||
.slow(factor);
|
.slow(factor);
|
||||||
}
|
}
|
||||||
onTrigger(onTrigger) {
|
onTrigger(onTrigger, dominant = true) {
|
||||||
return this._withHap((hap) => hap.setContext({ ...hap.context, onTrigger }));
|
|
||||||
}
|
|
||||||
log(func = id) {
|
|
||||||
return this._withHap((hap) =>
|
return this._withHap((hap) =>
|
||||||
hap.setContext({
|
hap.setContext({
|
||||||
...hap.context,
|
...hap.context,
|
||||||
onTrigger: (...args) => {
|
onTrigger: (...args) => {
|
||||||
if (hap.context.onTrigger) {
|
if (!dominant && hap.context.onTrigger) {
|
||||||
hap.context.onTrigger(...args);
|
hap.context.onTrigger(...args);
|
||||||
}
|
}
|
||||||
console.log(func(...args));
|
onTrigger(...args);
|
||||||
},
|
},
|
||||||
|
// we need this to know later if the default trigger should still fire
|
||||||
|
dominantTrigger: dominant,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log(func = (_, hap) => `[hap] ${hap.showWhole(true)}`) {
|
||||||
|
return this.onTrigger((...args) => logger(func(...args)), false);
|
||||||
|
}
|
||||||
logValues(func = id) {
|
logValues(func = id) {
|
||||||
return this.log((_, hap) => func(hap.value));
|
return this.log((_, hap) => func(hap.value));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
|
|||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Pattern } from '@strudel.cycles/core';
|
import { Pattern } from './index.mjs';
|
||||||
|
|
||||||
const scale = (normalized, min, max) => normalized * (max - min) + min;
|
const scale = (normalized, min, max) => normalized * (max - min) + min;
|
||||||
const getValue = (e) => {
|
const getValue = (e) => {
|
||||||
@ -1,23 +1,59 @@
|
|||||||
import { Cyclist } from './cyclist.mjs';
|
import { Cyclist } from './cyclist.mjs';
|
||||||
import { evaluate as _evaluate } from './evaluate.mjs';
|
import { evaluate as _evaluate } from './evaluate.mjs';
|
||||||
|
import { logger } from './logger.mjs';
|
||||||
|
import { setTime } from './time.mjs';
|
||||||
|
|
||||||
export function repl({ interval, defaultOutput, onSchedulerError, onEvalError, onEval, getTime, transpiler }) {
|
export function repl({
|
||||||
const scheduler = new Cyclist({ interval, onTrigger: defaultOutput, onError: onSchedulerError, getTime });
|
interval,
|
||||||
const evaluate = async (code) => {
|
defaultOutput,
|
||||||
|
onSchedulerError,
|
||||||
|
onEvalError,
|
||||||
|
beforeEval,
|
||||||
|
afterEval,
|
||||||
|
getTime,
|
||||||
|
transpiler,
|
||||||
|
onToggle,
|
||||||
|
}) {
|
||||||
|
const scheduler = new Cyclist({
|
||||||
|
interval,
|
||||||
|
onTrigger: async (hap, deadline, duration) => {
|
||||||
|
try {
|
||||||
|
if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
|
||||||
|
await defaultOutput(hap, deadline, duration);
|
||||||
|
}
|
||||||
|
if (hap.context.onTrigger) {
|
||||||
|
const cps = 1;
|
||||||
|
// 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,
|
||||||
|
});
|
||||||
|
setTime(() => scheduler.getPhase()); // TODO: refactor?
|
||||||
|
const evaluate = async (code, autostart = true) => {
|
||||||
if (!code) {
|
if (!code) {
|
||||||
throw new Error('no code to evaluate');
|
throw new Error('no code to evaluate');
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
beforeEval({ code });
|
||||||
const { pattern } = await _evaluate(code, transpiler);
|
const { pattern } = await _evaluate(code, transpiler);
|
||||||
scheduler.setPattern(pattern, true);
|
logger(`[eval] code updated`);
|
||||||
onEval?.({
|
scheduler.setPattern(pattern, autostart);
|
||||||
pattern,
|
afterEval({ code, pattern });
|
||||||
code,
|
return pattern;
|
||||||
});
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn(`eval error: ${err.message}`);
|
// console.warn(`[repl] eval error: ${err.message}`);
|
||||||
|
logger(`[eval] error: ${err.message}`, 'error');
|
||||||
onEvalError?.(err);
|
onEvalError?.(err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return { scheduler, evaluate };
|
const stop = () => scheduler.stop();
|
||||||
|
const start = () => scheduler.start();
|
||||||
|
const pause = () => scheduler.pause();
|
||||||
|
return { scheduler, evaluate, start, stop, pause };
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,11 +32,8 @@ function speak(words, lang, voice) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Pattern.prototype._speak = function (lang, voice) {
|
Pattern.prototype._speak = function (lang, voice) {
|
||||||
return this._withHap((hap) => {
|
return this.onTrigger((_, hap) => {
|
||||||
const onTrigger = (time, hap) => {
|
speak(hap.value, lang, voice);
|
||||||
speak(hap.value, lang, voice);
|
|
||||||
};
|
|
||||||
return hap.setContext({ ...hap.context, onTrigger });
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
11
packages/core/time.mjs
Normal file
11
packages/core/time.mjs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
let time;
|
||||||
|
export function getTime() {
|
||||||
|
if (!time) {
|
||||||
|
throw new Error('no time set! use setTime to define a time source');
|
||||||
|
}
|
||||||
|
return time();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setTime(func) {
|
||||||
|
time = func;
|
||||||
|
}
|
||||||
@ -4,19 +4,14 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
|
|||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Tone } from './tone.mjs';
|
import { getTime } from './time.mjs';
|
||||||
|
|
||||||
export const hideHeader = () => {
|
|
||||||
document.getElementById('header').style = 'display:none';
|
|
||||||
};
|
|
||||||
|
|
||||||
function frame(callback) {
|
function frame(callback) {
|
||||||
if (window.strudelAnimation) {
|
if (window.strudelAnimation) {
|
||||||
cancelAnimationFrame(window.strudelAnimation);
|
cancelAnimationFrame(window.strudelAnimation);
|
||||||
}
|
}
|
||||||
const animate = (animationTime) => {
|
const animate = (animationTime) => {
|
||||||
const toneTime = Tone.getTransport().seconds;
|
callback(animationTime, getTime());
|
||||||
callback(animationTime, toneTime);
|
|
||||||
window.strudelAnimation = requestAnimationFrame(animate);
|
window.strudelAnimation = requestAnimationFrame(animate);
|
||||||
};
|
};
|
||||||
requestAnimationFrame(animate);
|
requestAnimationFrame(animate);
|
||||||
@ -51,6 +46,7 @@ export const cleanupUi = () => {
|
|||||||
const container = document.getElementById('code');
|
const container = document.getElementById('code');
|
||||||
if (container) {
|
if (container) {
|
||||||
container.style = '';
|
container.style = '';
|
||||||
container.className = 'grow relative'; // has to match App.tsx
|
// TODO: find a way to remove that duplication..
|
||||||
|
container.className = 'grow flex text-gray-100 relative overflow-auto cursor-text pb-0'; // has to match App.tsx
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -55,9 +55,13 @@ export const midi2note = (n) => {
|
|||||||
export const mod = (n, m) => ((n % m) + m) % m;
|
export const mod = (n, m) => ((n % m) + m) % m;
|
||||||
|
|
||||||
export const getPlayableNoteValue = (hap) => {
|
export const getPlayableNoteValue = (hap) => {
|
||||||
let { value: note, context } = hap;
|
let { value, context } = hap;
|
||||||
|
let note = value;
|
||||||
if (typeof note === 'object' && !Array.isArray(note)) {
|
if (typeof note === 'object' && !Array.isArray(note)) {
|
||||||
note = note.note || note.n || note.value;
|
note = note.note || note.n || note.value;
|
||||||
|
if (note === undefined) {
|
||||||
|
throw new Error(`cannot find a playable note for ${JSON.stringify(value)}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// if value is number => interpret as midi number as long as its not marked as frequency
|
// if value is number => interpret as midi number as long as its not marked as frequency
|
||||||
if (typeof note === 'number' && context.type !== 'frequency') {
|
if (typeof note === 'number' && context.type !== 'frequency') {
|
||||||
|
|||||||
@ -31,10 +31,11 @@ function createClock(
|
|||||||
};
|
};
|
||||||
let intervalID;
|
let intervalID;
|
||||||
const start = () => {
|
const start = () => {
|
||||||
|
clear(); // just in case start was called more than once
|
||||||
onTick();
|
onTick();
|
||||||
intervalID = setInterval(onTick, interval * 1000);
|
intervalID = setInterval(onTick, interval * 1000);
|
||||||
};
|
};
|
||||||
const clear = () => clearInterval(intervalID);
|
const clear = () => intervalID !== undefined && clearInterval(intervalID);
|
||||||
const pause = () => clear();
|
const pause = () => clear();
|
||||||
const stop = () => {
|
const stop = () => {
|
||||||
tick = 0;
|
tick = 0;
|
||||||
|
|||||||
@ -4,14 +4,19 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
|
|||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { isNote } from 'tone';
|
|
||||||
import * as _WebMidi from 'webmidi';
|
import * as _WebMidi from 'webmidi';
|
||||||
import { Pattern, isPattern } from '@strudel.cycles/core';
|
import { Pattern, isPattern, isNote, getPlayableNoteValue, logger } from '@strudel.cycles/core';
|
||||||
import { Tone } from '@strudel.cycles/tone';
|
import { getAudioContext } from '@strudel.cycles/webaudio';
|
||||||
|
|
||||||
// if you use WebMidi from outside of this package, make sure to import that instance:
|
// if you use WebMidi from outside of this package, make sure to import that instance:
|
||||||
export const { WebMidi } = _WebMidi;
|
export const { WebMidi } = _WebMidi;
|
||||||
|
|
||||||
export function enableWebMidi() {
|
export function enableWebMidi(options = {}) {
|
||||||
|
const { onReady, onConnected, onDisconnected } = options;
|
||||||
|
|
||||||
|
if (typeof navigator.requestMIDIAccess !== 'function') {
|
||||||
|
throw new Error('Your Browser does not support WebMIDI.');
|
||||||
|
}
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (WebMidi.enabled) {
|
if (WebMidi.enabled) {
|
||||||
// if already enabled, just resolve WebMidi
|
// if already enabled, just resolve WebMidi
|
||||||
@ -22,6 +27,14 @@ export function enableWebMidi() {
|
|||||||
if (err) {
|
if (err) {
|
||||||
reject(err);
|
reject(err);
|
||||||
}
|
}
|
||||||
|
WebMidi.addListener('connected', (e) => {
|
||||||
|
onConnected?.(WebMidi);
|
||||||
|
});
|
||||||
|
// Reacting when a device becomes unavailable
|
||||||
|
WebMidi.addListener('disconnected', (e) => {
|
||||||
|
onDisconnected?.(WebMidi, e);
|
||||||
|
});
|
||||||
|
onReady?.(WebMidi);
|
||||||
resolve(WebMidi);
|
resolve(WebMidi);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -30,7 +43,21 @@ export function enableWebMidi() {
|
|||||||
const outputByName = (name) => WebMidi.getOutputByName(name);
|
const outputByName = (name) => WebMidi.getOutputByName(name);
|
||||||
|
|
||||||
// Pattern.prototype.midi = function (output: string | number, channel = 1) {
|
// Pattern.prototype.midi = function (output: string | number, channel = 1) {
|
||||||
Pattern.prototype.midi = function (output, channel = 1) {
|
Pattern.prototype.midi = async function (output, channel = 1) {
|
||||||
|
await enableWebMidi({
|
||||||
|
onConnected: ({ outputs }) =>
|
||||||
|
logger(`Midi device connected! Available: ${outputs.map((o) => `'${o.name}'`).join(', ')}`),
|
||||||
|
onDisconnected: ({ outputs }) =>
|
||||||
|
logger(`Midi device disconnected! Available: ${outputs.map((o) => `'${o.name}'`).join(', ')}`),
|
||||||
|
onReady: ({ outputs }) => {
|
||||||
|
const chosenOutput = output ?? outputs[0];
|
||||||
|
const otherOutputs = outputs
|
||||||
|
.filter((o) => o.name !== chosenOutput.name)
|
||||||
|
.map((o) => `'${o.name}'`)
|
||||||
|
.join(' | ');
|
||||||
|
logger(`Midi connected! Using "${chosenOutput.name}". ${otherOutputs ? `Also available: ${otherOutputs}` : ''}`);
|
||||||
|
},
|
||||||
|
});
|
||||||
if (isPattern(output?.constructor?.name)) {
|
if (isPattern(output?.constructor?.name)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`.midi does not accept Pattern input. Make sure to pass device name with single quotes. Example: .midi('${
|
`.midi does not accept Pattern input. Make sure to pass device name with single quotes. Example: .midi('${
|
||||||
@ -38,46 +65,42 @@ Pattern.prototype.midi = function (output, channel = 1) {
|
|||||||
}')`,
|
}')`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return this._withHap((hap) => {
|
return this.onTrigger((time, hap) => {
|
||||||
// const onTrigger = (time: number, hap: any) => {
|
let note = getPlayableNoteValue(hap);
|
||||||
const onTrigger = (time, hap) => {
|
const velocity = hap.context?.velocity ?? 0.9;
|
||||||
let note = hap.value;
|
if (!isNote(note)) {
|
||||||
const velocity = hap.context?.velocity ?? 0.9;
|
throw new Error('not a note: ' + note);
|
||||||
if (!isNote(note)) {
|
}
|
||||||
throw new Error('not a note: ' + note);
|
if (!WebMidi.enabled) {
|
||||||
}
|
throw new Error(`🎹 WebMidi is not enabled. Supported Browsers: https://caniuse.com/?search=webmidi`);
|
||||||
if (!WebMidi.enabled) {
|
}
|
||||||
throw new Error(`🎹 WebMidi is not enabled. Supported Browsers: https://caniuse.com/?search=webmidi`);
|
if (!WebMidi.outputs.length) {
|
||||||
}
|
throw new Error(`🔌 No MIDI devices found. Connect a device or enable IAC Driver.`);
|
||||||
if (!WebMidi.outputs.length) {
|
}
|
||||||
throw new Error(`🔌 No MIDI devices found. Connect a device or enable IAC Driver.`);
|
let device;
|
||||||
}
|
if (typeof output === 'number') {
|
||||||
let device;
|
device = WebMidi.outputs[output];
|
||||||
if (typeof output === 'number') {
|
} else if (typeof output === 'string') {
|
||||||
device = WebMidi.outputs[output];
|
device = outputByName(output);
|
||||||
} else if (typeof output === 'string') {
|
} else {
|
||||||
device = outputByName(output);
|
device = WebMidi.outputs[0];
|
||||||
} else {
|
}
|
||||||
device = WebMidi.outputs[0];
|
if (!device) {
|
||||||
}
|
throw new Error(
|
||||||
if (!device) {
|
`🔌 MIDI device '${output ? output : ''}' not found. Use one of ${WebMidi.outputs
|
||||||
throw new Error(
|
.map((o) => `'${o.name}'`)
|
||||||
`🔌 MIDI device '${output ? output : ''}' not found. Use one of ${WebMidi.outputs
|
.join(' | ')}`,
|
||||||
.map((o) => `'${o.name}'`)
|
);
|
||||||
.join(' | ')}`,
|
}
|
||||||
);
|
// console.log('midi', value, output);
|
||||||
}
|
const timingOffset = WebMidi.time - getAudioContext().currentTime * 1000;
|
||||||
// console.log('midi', value, output);
|
time = time * 1000 + timingOffset;
|
||||||
const timingOffset = WebMidi.time - Tone.getContext().currentTime * 1000;
|
// const inMs = '+' + (time - Tone.getContext().currentTime) * 1000;
|
||||||
time = time * 1000 + timingOffset;
|
// await enableWebMidi()
|
||||||
// const inMs = '+' + (time - Tone.getContext().currentTime) * 1000;
|
device.playNote(note, channel, {
|
||||||
// await enableWebMidi()
|
time,
|
||||||
device.playNote(note, channel, {
|
duration: hap.duration.valueOf() * 1000 - 5,
|
||||||
time,
|
attack: velocity,
|
||||||
duration: hap.duration.valueOf() * 1000 - 5,
|
});
|
||||||
velocity,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
return hap.setContext({ ...hap.context, onTrigger });
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -32,7 +32,7 @@ function peg$padEnd(str, targetLength, padString) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
peg$SyntaxError.prototype.format = function(sources) {
|
peg$SyntaxError.prototype.format = function(sources) {
|
||||||
var str = "Error: " + this.message;
|
var str = "peg error: " + this.message;
|
||||||
if (this.location) {
|
if (this.location) {
|
||||||
var src = null;
|
var src = null;
|
||||||
var k;
|
var k;
|
||||||
|
|||||||
@ -5,10 +5,34 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import OSC from 'osc-js';
|
import OSC from 'osc-js';
|
||||||
import { parseNumeral, Pattern } from '@strudel.cycles/core';
|
import { logger, parseNumeral, Pattern } from '@strudel.cycles/core';
|
||||||
|
|
||||||
|
let connection; // Promise<OSC>
|
||||||
|
function connect() {
|
||||||
|
if (!connection) {
|
||||||
|
// make sure this runs only once
|
||||||
|
connection = new Promise((resolve, reject) => {
|
||||||
|
const osc = new OSC();
|
||||||
|
osc.open();
|
||||||
|
osc.on('open', () => {
|
||||||
|
const url = osc.options?.plugin?.socket?.url;
|
||||||
|
logger(`[osc] connected${url ? ` to ${url}` : ''}`);
|
||||||
|
resolve(osc);
|
||||||
|
});
|
||||||
|
osc.on('close', () => {
|
||||||
|
connection = undefined; // allows new connection afterwards
|
||||||
|
console.log('[osc] disconnected');
|
||||||
|
reject('OSC connection closed');
|
||||||
|
});
|
||||||
|
osc.on('error', (err) => reject(err));
|
||||||
|
}).catch((err) => {
|
||||||
|
connection = undefined;
|
||||||
|
throw new Error('Could not connect to OSC server. Is it running?');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return connection;
|
||||||
|
}
|
||||||
|
|
||||||
const comm = new OSC();
|
|
||||||
comm.open();
|
|
||||||
const latency = 0.1;
|
const latency = 0.1;
|
||||||
let startedAt = -1;
|
let startedAt = -1;
|
||||||
|
|
||||||
@ -20,28 +44,25 @@ let startedAt = -1;
|
|||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
*/
|
*/
|
||||||
Pattern.prototype.osc = function () {
|
Pattern.prototype.osc = async function () {
|
||||||
return this._withHap((hap) => {
|
const osc = await connect();
|
||||||
const onTrigger = (time, hap, currentTime, cps) => {
|
return this.onTrigger((time, hap, currentTime, cps = 1) => {
|
||||||
const cycle = hap.wholeOrPart().begin.valueOf();
|
const cycle = hap.wholeOrPart().begin.valueOf();
|
||||||
const delta = hap.duration.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, cycle, delta }, hap.value);
|
const controls = Object.assign({}, { cps, cycle, delta }, hap.value);
|
||||||
// make sure n and note are numbers
|
// make sure n and note are numbers
|
||||||
controls.n && (controls.n = parseNumeral(controls.n));
|
controls.n && (controls.n = parseNumeral(controls.n));
|
||||||
controls.note && (controls.note = parseNumeral(controls.note));
|
controls.note && (controls.note = parseNumeral(controls.note));
|
||||||
|
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);
|
const bundle = new OSC.Bundle([message], ts);
|
||||||
const bundle = new OSC.Bundle([message], ts);
|
bundle.timestamp(ts); // workaround for https://github.com/adzialocha/osc-js/issues/60
|
||||||
bundle.timestamp(ts); // workaround for https://github.com/adzialocha/osc-js/issues/60
|
osc.send(bundle);
|
||||||
comm.send(bundle);
|
|
||||||
};
|
|
||||||
return hap.setContext({ ...hap.context, onTrigger });
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
4
packages/react/dist/index.cjs.js
vendored
4
packages/react/dist/index.cjs.js
vendored
File diff suppressed because one or more lines are too long
630
packages/react/dist/index.es.js
vendored
630
packages/react/dist/index.es.js
vendored
@ -1,17 +1,15 @@
|
|||||||
import y, { useCallback as T, useState as w, useEffect as q, useMemo as R, useRef as I, useLayoutEffect as j } from "react";
|
import n, { useCallback as N, useRef as x, useEffect as R, useState as w, useMemo as I, useLayoutEffect as K } from "react";
|
||||||
import Y from "@uiw/react-codemirror";
|
import Q from "@uiw/react-codemirror";
|
||||||
import { Decoration as A, EditorView as K } from "@codemirror/view";
|
import { Decoration as E, EditorView as O } from "@codemirror/view";
|
||||||
import { StateEffect as U, StateField as Q } from "@codemirror/state";
|
import { StateEffect as j, StateField as U } from "@codemirror/state";
|
||||||
import { javascript as Z } from "@codemirror/lang-javascript";
|
import { javascript as W } from "@codemirror/lang-javascript";
|
||||||
import { tags as m } from "@lezer/highlight";
|
import { tags as r } from "@lezer/highlight";
|
||||||
import { createTheme as ee } from "@uiw/codemirror-themes";
|
import { createTheme as X } from "@uiw/codemirror-themes";
|
||||||
import { useInView as te } from "react-hook-inview";
|
import { useInView as Y } from "react-hook-inview";
|
||||||
import { evaluate as G } from "@strudel.cycles/eval";
|
import { webaudioOutput as Z, getAudioContext as ee } from "@strudel.cycles/webaudio";
|
||||||
import { Tone as M } from "@strudel.cycles/tone";
|
import { repl as te } from "@strudel.cycles/core";
|
||||||
import { TimeSpan as oe, State as re } from "@strudel.cycles/core";
|
import { transpiler as re } from "@strudel.cycles/transpiler";
|
||||||
import { webaudioOutputTrigger as ne } from "@strudel.cycles/webaudio";
|
const oe = X({
|
||||||
import { WebMidi as N, enableWebMidi as se } from "@strudel.cycles/midi";
|
|
||||||
const ae = ee({
|
|
||||||
theme: "dark",
|
theme: "dark",
|
||||||
settings: {
|
settings: {
|
||||||
background: "#222",
|
background: "#222",
|
||||||
@ -19,450 +17,294 @@ const ae = ee({
|
|||||||
caret: "#ffcc00",
|
caret: "#ffcc00",
|
||||||
selection: "rgba(128, 203, 196, 0.5)",
|
selection: "rgba(128, 203, 196, 0.5)",
|
||||||
selectionMatch: "#036dd626",
|
selectionMatch: "#036dd626",
|
||||||
lineHighlight: "#8a91991a",
|
lineHighlight: "#00000050",
|
||||||
gutterBackground: "transparent",
|
gutterBackground: "transparent",
|
||||||
gutterForeground: "#676e95"
|
gutterForeground: "#8a919966"
|
||||||
},
|
},
|
||||||
styles: [
|
styles: [
|
||||||
{ tag: m.keyword, color: "#c792ea" },
|
{ tag: r.keyword, color: "#c792ea" },
|
||||||
{ tag: m.operator, color: "#89ddff" },
|
{ tag: r.operator, color: "#89ddff" },
|
||||||
{ tag: m.special(m.variableName), color: "#eeffff" },
|
{ tag: r.special(r.variableName), color: "#eeffff" },
|
||||||
{ tag: m.typeName, color: "#f07178" },
|
{ tag: r.typeName, color: "#c3e88d" },
|
||||||
{ tag: m.atom, color: "#f78c6c" },
|
{ tag: r.atom, color: "#f78c6c" },
|
||||||
{ tag: m.number, color: "#ff5370" },
|
{ tag: r.number, color: "#c3e88d" },
|
||||||
{ tag: m.definition(m.variableName), color: "#82aaff" },
|
{ tag: r.definition(r.variableName), color: "#82aaff" },
|
||||||
{ tag: m.string, color: "#c3e88d" },
|
{ tag: r.string, color: "#c3e88d" },
|
||||||
{ tag: m.special(m.string), color: "#f07178" },
|
{ tag: r.special(r.string), color: "#c3e88d" },
|
||||||
{ tag: m.comment, color: "#7d8799" },
|
{ tag: r.comment, color: "#7d8799" },
|
||||||
{ tag: m.variableName, color: "#f07178" },
|
{ tag: r.variableName, color: "#c792ea" },
|
||||||
{ tag: m.tagName, color: "#ff5370" },
|
{ tag: r.tagName, color: "#c3e88d" },
|
||||||
{ tag: m.bracket, color: "#a2a1a4" },
|
{ tag: r.bracket, color: "#525154" },
|
||||||
{ tag: m.meta, color: "#ffcb6b" },
|
{ tag: r.meta, color: "#ffcb6b" },
|
||||||
{ tag: m.attributeName, color: "#c792ea" },
|
{ tag: r.attributeName, color: "#c792ea" },
|
||||||
{ tag: m.propertyName, color: "#c792ea" },
|
{ tag: r.propertyName, color: "#c792ea" },
|
||||||
{ tag: m.className, color: "#decb6b" },
|
{ tag: r.className, color: "#decb6b" },
|
||||||
{ tag: m.invalid, color: "#ffffff" }
|
{ tag: r.invalid, color: "#ffffff" }
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
const z = U.define(), ce = Q.define({
|
const T = j.define(), ne = U.define({
|
||||||
create() {
|
create() {
|
||||||
return A.none;
|
return E.none;
|
||||||
},
|
},
|
||||||
update(e, o) {
|
update(e, t) {
|
||||||
try {
|
try {
|
||||||
for (let r of o.effects)
|
for (let o of t.effects)
|
||||||
if (r.is(z))
|
if (o.is(T))
|
||||||
if (r.value) {
|
if (o.value) {
|
||||||
const n = A.mark({ attributes: { style: "background-color: #FFCA2880" } });
|
const a = E.mark({ attributes: { style: "background-color: #FFCA2880" } });
|
||||||
e = A.set([n.range(0, o.newDoc.length)]);
|
e = E.set([a.range(0, t.newDoc.length)]);
|
||||||
} else
|
} else
|
||||||
e = A.set([]);
|
e = E.set([]);
|
||||||
return e;
|
return e;
|
||||||
} catch (r) {
|
} catch (o) {
|
||||||
return console.warn("flash error", r), e;
|
return console.warn("flash error", o), e;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
provide: (e) => K.decorations.from(e)
|
provide: (e) => O.decorations.from(e)
|
||||||
}), ie = (e) => {
|
}), ae = (e) => {
|
||||||
e.dispatch({ effects: z.of(!0) }), setTimeout(() => {
|
e.dispatch({ effects: T.of(!0) }), setTimeout(() => {
|
||||||
e.dispatch({ effects: z.of(!1) });
|
e.dispatch({ effects: T.of(!1) });
|
||||||
}, 200);
|
}, 200);
|
||||||
}, O = U.define(), le = Q.define({
|
}, A = j.define(), se = U.define({
|
||||||
create() {
|
create() {
|
||||||
return A.none;
|
return E.none;
|
||||||
},
|
},
|
||||||
update(e, o) {
|
update(e, t) {
|
||||||
try {
|
try {
|
||||||
for (let r of o.effects)
|
for (let o of t.effects)
|
||||||
if (r.is(O)) {
|
if (o.is(A)) {
|
||||||
const n = r.value.map(
|
const a = o.value.map(
|
||||||
(s) => (s.context.locations || []).map(({ start: i, end: a }) => {
|
(s) => (s.context.locations || []).map(({ start: m, end: l }) => {
|
||||||
const t = s.context.color || "#FFCA28";
|
const d = s.context.color || "#FFCA28";
|
||||||
let l = o.newDoc.line(i.line).from + i.column, c = o.newDoc.line(a.line).from + a.column;
|
let c = t.newDoc.line(m.line).from + m.column, i = t.newDoc.line(l.line).from + l.column;
|
||||||
const f = o.newDoc.length;
|
const g = t.newDoc.length;
|
||||||
return l > f || c > f ? void 0 : A.mark({ attributes: { style: `outline: 1.5px solid ${t};` } }).range(l, c);
|
return c > g || i > g ? void 0 : E.mark({ attributes: { style: `outline: 1.5px solid ${d};` } }).range(c, i);
|
||||||
})
|
})
|
||||||
).flat().filter(Boolean) || [];
|
).flat().filter(Boolean) || [];
|
||||||
e = A.set(n, !0);
|
e = E.set(a, !0);
|
||||||
}
|
}
|
||||||
return e;
|
return e;
|
||||||
} catch {
|
} catch {
|
||||||
return A.set([]);
|
return E.set([]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
provide: (e) => K.decorations.from(e)
|
provide: (e) => O.decorations.from(e)
|
||||||
}), ue = [Z(), ae, le, ce];
|
}), ce = [W(), oe, se, ne];
|
||||||
function de({ value: e, onChange: o, onViewChanged: r, onSelectionChange: n, options: s, editorDidMount: i }) {
|
function ie({ value: e, onChange: t, onViewChanged: o, onSelectionChange: a, options: s, editorDidMount: m }) {
|
||||||
const a = T(
|
const l = N(
|
||||||
(c) => {
|
(i) => {
|
||||||
o?.(c);
|
t?.(i);
|
||||||
|
},
|
||||||
|
[t]
|
||||||
|
), d = N(
|
||||||
|
(i) => {
|
||||||
|
o?.(i);
|
||||||
},
|
},
|
||||||
[o]
|
[o]
|
||||||
), t = T(
|
), c = N(
|
||||||
(c) => {
|
(i) => {
|
||||||
r?.(c);
|
i.selectionSet && a && a?.(i.state.selection);
|
||||||
},
|
},
|
||||||
[r]
|
[a]
|
||||||
), l = T(
|
|
||||||
(c) => {
|
|
||||||
c.selectionSet && n && n?.(c.state.selection);
|
|
||||||
},
|
|
||||||
[n]
|
|
||||||
);
|
);
|
||||||
return /* @__PURE__ */ y.createElement(y.Fragment, null, /* @__PURE__ */ y.createElement(Y, {
|
return /* @__PURE__ */ n.createElement(n.Fragment, null, /* @__PURE__ */ n.createElement(Q, {
|
||||||
value: e,
|
value: e,
|
||||||
onChange: a,
|
onChange: l,
|
||||||
onCreateEditor: t,
|
onCreateEditor: d,
|
||||||
onUpdate: l,
|
onUpdate: c,
|
||||||
extensions: ue
|
extensions: ce
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
function fe(e) {
|
function B(...e) {
|
||||||
const { onEvent: o, onQuery: r, onSchedule: n, ready: s = !0, onDraw: i } = e, [a, t] = w(!1), l = 1, c = () => Math.floor(M.getTransport().seconds / l), f = (g = c()) => {
|
|
||||||
const C = new oe(g, g + 1), v = r?.(new re(C)) || [];
|
|
||||||
n?.(v, g);
|
|
||||||
const h = C.begin.valueOf();
|
|
||||||
M.getTransport().cancel(h);
|
|
||||||
const D = (g + 1) * l - 0.5, _ = Math.max(M.getTransport().seconds, D) + 0.1;
|
|
||||||
M.getTransport().schedule(() => {
|
|
||||||
f(g + 1);
|
|
||||||
}, _), v?.filter((E) => E.part.begin.equals(E.whole?.begin)).forEach((E) => {
|
|
||||||
M.getTransport().schedule((L) => {
|
|
||||||
o(L, E, M.getContext().currentTime), M.Draw.schedule(() => {
|
|
||||||
i?.(L, E);
|
|
||||||
}, L);
|
|
||||||
}, E.part.begin.valueOf());
|
|
||||||
});
|
|
||||||
};
|
|
||||||
q(() => {
|
|
||||||
s && f();
|
|
||||||
}, [o, n, r, i, s]);
|
|
||||||
const p = async () => {
|
|
||||||
t(!0), await M.start(), M.getTransport().start("+0.1");
|
|
||||||
}, d = () => {
|
|
||||||
M.getTransport().pause(), t(!1);
|
|
||||||
};
|
|
||||||
return {
|
|
||||||
start: p,
|
|
||||||
stop: d,
|
|
||||||
onEvent: o,
|
|
||||||
started: a,
|
|
||||||
setStarted: t,
|
|
||||||
toggle: () => a ? d() : p(),
|
|
||||||
query: f,
|
|
||||||
activeCycle: c
|
|
||||||
};
|
|
||||||
}
|
|
||||||
function ge(e) {
|
|
||||||
return q(() => (window.addEventListener("message", e), () => window.removeEventListener("message", e)), [e]), T((o) => window.postMessage(o, "*"), []);
|
|
||||||
}
|
|
||||||
let he = () => Math.floor((1 + Math.random()) * 65536).toString(16).substring(1);
|
|
||||||
const me = (e) => encodeURIComponent(btoa(e));
|
|
||||||
function pe({ tune: e, autolink: o = !0, onEvent: r, onDraw: n }) {
|
|
||||||
const s = R(() => he(), []), [i, a] = w(e), [t, l] = w(), [c, f] = w(""), [p, d] = w(), [k, g] = w(!1), [C, v] = w(""), [h, D] = w(), _ = R(() => i !== t || p, [i, t, p]), E = T((b) => f((u) => u + `${u ? `
|
|
||||||
|
|
||||||
` : ""}${b}`), []), L = R(() => {
|
|
||||||
if (t && !t.includes("strudel disable-highlighting"))
|
|
||||||
return (b, u) => n?.(b, u, t);
|
|
||||||
}, [t, n]), H = R(() => t && t.includes("strudel hide-header"), [t]), x = R(() => t && t.includes("strudel hide-console"), [t]), P = fe({
|
|
||||||
onDraw: L,
|
|
||||||
onEvent: T(
|
|
||||||
(b, u, X) => {
|
|
||||||
try {
|
|
||||||
r?.(u), u.context.logs?.length && u.context.logs.forEach(E);
|
|
||||||
const { onTrigger: S = ne } = u.context;
|
|
||||||
S(b, u, X, 1);
|
|
||||||
} catch (S) {
|
|
||||||
console.warn(S), S.message = "unplayable event: " + S?.message, E(S.message);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[r, E]
|
|
||||||
),
|
|
||||||
onQuery: T(
|
|
||||||
(b) => {
|
|
||||||
try {
|
|
||||||
return h?.query(b) || [];
|
|
||||||
} catch (u) {
|
|
||||||
return console.warn(u), u.message = "query error: " + u.message, d(u), [];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[h]
|
|
||||||
),
|
|
||||||
onSchedule: T((b, u) => J(b), []),
|
|
||||||
ready: !!h && !!t
|
|
||||||
}), B = ge(({ data: { from: b, type: u } }) => {
|
|
||||||
u === "start" && b !== s && (P.setStarted(!1), l(void 0));
|
|
||||||
}), V = T(
|
|
||||||
async (b = i) => {
|
|
||||||
if (t && !_) {
|
|
||||||
d(void 0), P.start();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
g(!0);
|
|
||||||
const u = await G(b);
|
|
||||||
P.start(), B({ type: "start", from: s }), D(() => u.pattern), o && (window.location.hash = "#" + encodeURIComponent(btoa(i))), v(me(i)), d(void 0), l(b), g(!1);
|
|
||||||
} catch (u) {
|
|
||||||
u.message = "evaluation error: " + u.message, console.warn(u), d(u);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[t, _, i, P, o, s, B]
|
|
||||||
), J = (b, u) => {
|
|
||||||
b.length;
|
|
||||||
};
|
|
||||||
return {
|
|
||||||
hideHeader: H,
|
|
||||||
hideConsole: x,
|
|
||||||
pending: k,
|
|
||||||
code: i,
|
|
||||||
setCode: a,
|
|
||||||
pattern: h,
|
|
||||||
error: p,
|
|
||||||
cycle: P,
|
|
||||||
setPattern: D,
|
|
||||||
dirty: _,
|
|
||||||
log: c,
|
|
||||||
togglePlay: () => {
|
|
||||||
P.started ? P.stop() : V();
|
|
||||||
},
|
|
||||||
setActiveCode: l,
|
|
||||||
activateCode: V,
|
|
||||||
activeCode: t,
|
|
||||||
pushLog: E,
|
|
||||||
hash: C
|
|
||||||
};
|
|
||||||
}
|
|
||||||
function $(...e) {
|
|
||||||
return e.filter(Boolean).join(" ");
|
return e.filter(Boolean).join(" ");
|
||||||
}
|
}
|
||||||
function ye({ view: e, pattern: o, active: r, getTime: n }) {
|
function le({ view: e, pattern: t, active: o, getTime: a }) {
|
||||||
const s = I([]), i = I();
|
const s = x([]), m = x();
|
||||||
q(() => {
|
R(() => {
|
||||||
if (e)
|
if (e)
|
||||||
if (o && r) {
|
if (t && o) {
|
||||||
let t = function() {
|
let d = function() {
|
||||||
try {
|
try {
|
||||||
const l = n(), f = [Math.max(i.current || l, l - 1 / 10), l + 1 / 60];
|
const c = a(), g = [Math.max(m.current || c, c - 1 / 10, 0), c + 1 / 60];
|
||||||
i.current = l + 1 / 60, s.current = s.current.filter((d) => d.whole.end > l);
|
m.current = g[1], s.current = s.current.filter((p) => p.whole.end > c);
|
||||||
const p = o.queryArc(...f).filter((d) => d.hasOnset());
|
const v = t.queryArc(...g).filter((p) => p.hasOnset());
|
||||||
s.current = s.current.concat(p), e.dispatch({ effects: O.of(s.current) });
|
s.current = s.current.concat(v), e.dispatch({ effects: A.of(s.current) });
|
||||||
} catch {
|
} catch {
|
||||||
e.dispatch({ effects: O.of([]) });
|
e.dispatch({ effects: A.of([]) });
|
||||||
}
|
}
|
||||||
a = requestAnimationFrame(t);
|
l = requestAnimationFrame(d);
|
||||||
}, a = requestAnimationFrame(t);
|
}, l = requestAnimationFrame(d);
|
||||||
return () => {
|
return () => {
|
||||||
cancelAnimationFrame(a);
|
cancelAnimationFrame(l);
|
||||||
};
|
};
|
||||||
} else
|
} else
|
||||||
s.current = [], e.dispatch({ effects: O.of([]) });
|
s.current = [], e.dispatch({ effects: A.of([]) });
|
||||||
}, [o, r, e]);
|
}, [t, o, e]);
|
||||||
}
|
}
|
||||||
const be = "_container_3i85k_1", we = "_header_3i85k_5", ve = "_buttons_3i85k_9", Ee = "_button_3i85k_9", ke = "_buttonDisabled_3i85k_17", Ce = "_error_3i85k_21", Me = "_body_3i85k_25", F = {
|
const de = "_container_3i85k_1", ue = "_header_3i85k_5", fe = "_buttons_3i85k_9", me = "_button_3i85k_9", ge = "_buttonDisabled_3i85k_17", pe = "_error_3i85k_21", he = "_body_3i85k_25", b = {
|
||||||
container: be,
|
container: de,
|
||||||
header: we,
|
header: ue,
|
||||||
buttons: ve,
|
buttons: fe,
|
||||||
button: Ee,
|
button: me,
|
||||||
buttonDisabled: ke,
|
buttonDisabled: ge,
|
||||||
error: Ce,
|
error: pe,
|
||||||
body: Me
|
body: he
|
||||||
};
|
};
|
||||||
function W({ type: e }) {
|
function q({ type: e }) {
|
||||||
return /* @__PURE__ */ y.createElement("svg", {
|
return /* @__PURE__ */ n.createElement("svg", {
|
||||||
xmlns: "http://www.w3.org/2000/svg",
|
xmlns: "http://www.w3.org/2000/svg",
|
||||||
className: "sc-h-5 sc-w-5",
|
className: "sc-h-5 sc-w-5",
|
||||||
viewBox: "0 0 20 20",
|
viewBox: "0 0 20 20",
|
||||||
fill: "currentColor"
|
fill: "currentColor"
|
||||||
}, {
|
}, {
|
||||||
refresh: /* @__PURE__ */ y.createElement("path", {
|
refresh: /* @__PURE__ */ n.createElement("path", {
|
||||||
fillRule: "evenodd",
|
fillRule: "evenodd",
|
||||||
d: "M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z",
|
d: "M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z",
|
||||||
clipRule: "evenodd"
|
clipRule: "evenodd"
|
||||||
}),
|
}),
|
||||||
play: /* @__PURE__ */ y.createElement("path", {
|
play: /* @__PURE__ */ n.createElement("path", {
|
||||||
fillRule: "evenodd",
|
fillRule: "evenodd",
|
||||||
d: "M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z",
|
d: "M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z",
|
||||||
clipRule: "evenodd"
|
clipRule: "evenodd"
|
||||||
}),
|
}),
|
||||||
pause: /* @__PURE__ */ y.createElement("path", {
|
pause: /* @__PURE__ */ n.createElement("path", {
|
||||||
fillRule: "evenodd",
|
fillRule: "evenodd",
|
||||||
d: "M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v4a1 1 0 102 0V8a1 1 0 00-1-1z",
|
d: "M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v4a1 1 0 102 0V8a1 1 0 00-1-1z",
|
||||||
clipRule: "evenodd"
|
clipRule: "evenodd"
|
||||||
})
|
})
|
||||||
}[e]);
|
}[e]);
|
||||||
}
|
}
|
||||||
function Be({ tune: e, hideOutsideView: o = !1, init: r, onEvent: n, enableKeyboard: s }) {
|
function ve({
|
||||||
const { code: i, setCode: a, pattern: t, activeCode: l, activateCode: c, evaluateOnly: f, error: p, cycle: d, dirty: k, togglePlay: g, stop: C } = pe({
|
defaultOutput: e,
|
||||||
tune: e,
|
interval: t,
|
||||||
autolink: !1,
|
getTime: o,
|
||||||
onEvent: n
|
evalOnMount: a = !1,
|
||||||
});
|
initialCode: s = "",
|
||||||
q(() => {
|
autolink: m = !1,
|
||||||
r && f();
|
beforeEval: l,
|
||||||
}, [e, r]);
|
afterEval: d,
|
||||||
const [v, h] = w(), [D, _] = te({
|
onEvalError: c,
|
||||||
threshold: 0.01
|
onToggle: i
|
||||||
}), E = I(), L = R(() => ((_ || !o) && (E.current = !0), _ || E.current), [_, o]);
|
}) {
|
||||||
return ye({
|
const [g, v] = w(), [p, D] = w(), [h, k] = w(s), [y, P] = w(h), [z, _] = w(), [C, H] = w(!1), F = h !== y, { scheduler: u, evaluate: L, start: $, stop: G, pause: J } = I(
|
||||||
view: v,
|
() => te({
|
||||||
pattern: t,
|
interval: t,
|
||||||
active: d.started && !l?.includes("strudel disable-highlighting"),
|
defaultOutput: e,
|
||||||
getTime: () => M.getTransport().seconds
|
onSchedulerError: v,
|
||||||
}), j(() => {
|
onEvalError: (f) => {
|
||||||
if (s) {
|
D(f), c?.(f);
|
||||||
const H = async (x) => {
|
},
|
||||||
(x.ctrlKey || x.altKey) && (x.code === "Enter" ? (x.preventDefault(), ie(v), await c()) : x.code === "Period" && (d.stop(), x.preventDefault()));
|
getTime: o,
|
||||||
};
|
transpiler: re,
|
||||||
return window.addEventListener("keydown", H, !0), () => window.removeEventListener("keydown", H, !0);
|
beforeEval: ({ code: f }) => {
|
||||||
|
k(f), l?.();
|
||||||
|
},
|
||||||
|
afterEval: ({ pattern: f, code: S }) => {
|
||||||
|
P(S), _(f), D(), v(), m && (window.location.hash = "#" + encodeURIComponent(btoa(S))), d?.();
|
||||||
|
},
|
||||||
|
onToggle: (f) => {
|
||||||
|
H(f), i?.(f);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
[e, t, o]
|
||||||
|
), M = N(async (f = !0) => L(h, f), [L, h]), V = x();
|
||||||
|
return R(() => {
|
||||||
|
!V.current && a && h && (V.current = !0, M());
|
||||||
|
}, [M, a, h]), R(() => () => {
|
||||||
|
u.stop();
|
||||||
|
}, [u]), {
|
||||||
|
code: h,
|
||||||
|
setCode: k,
|
||||||
|
error: g || p,
|
||||||
|
schedulerError: g,
|
||||||
|
scheduler: u,
|
||||||
|
evalError: p,
|
||||||
|
evaluate: L,
|
||||||
|
activateCode: M,
|
||||||
|
activeCode: y,
|
||||||
|
isDirty: F,
|
||||||
|
pattern: z,
|
||||||
|
started: C,
|
||||||
|
start: $,
|
||||||
|
stop: G,
|
||||||
|
pause: J,
|
||||||
|
togglePlay: async () => {
|
||||||
|
C ? u.pause() : await M();
|
||||||
}
|
}
|
||||||
}, [s, t, i, c, d, v]), /* @__PURE__ */ y.createElement("div", {
|
};
|
||||||
className: F.container,
|
}
|
||||||
ref: D
|
const be = () => ee().currentTime;
|
||||||
}, /* @__PURE__ */ y.createElement("div", {
|
function Pe({ tune: e, hideOutsideView: t = !1, init: o, enableKeyboard: a }) {
|
||||||
className: F.header
|
const {
|
||||||
}, /* @__PURE__ */ y.createElement("div", {
|
code: s,
|
||||||
className: F.buttons
|
setCode: m,
|
||||||
}, /* @__PURE__ */ y.createElement("button", {
|
evaluate: l,
|
||||||
className: $(F.button, d.started ? "sc-animate-pulse" : ""),
|
activateCode: d,
|
||||||
onClick: () => g()
|
error: c,
|
||||||
}, /* @__PURE__ */ y.createElement(W, {
|
isDirty: i,
|
||||||
type: d.started ? "pause" : "play"
|
activeCode: g,
|
||||||
})), /* @__PURE__ */ y.createElement("button", {
|
pattern: v,
|
||||||
className: $(k ? F.button : F.buttonDisabled),
|
started: p,
|
||||||
onClick: () => c()
|
scheduler: D,
|
||||||
}, /* @__PURE__ */ y.createElement(W, {
|
togglePlay: h,
|
||||||
|
stop: k
|
||||||
|
} = ve({
|
||||||
|
initialCode: e,
|
||||||
|
defaultOutput: Z,
|
||||||
|
getTime: be
|
||||||
|
}), [y, P] = w(), [z, _] = Y({
|
||||||
|
threshold: 0.01
|
||||||
|
}), C = x(), H = I(() => ((_ || !t) && (C.current = !0), _ || C.current), [_, t]);
|
||||||
|
return le({
|
||||||
|
view: y,
|
||||||
|
pattern: v,
|
||||||
|
active: p && !g?.includes("strudel disable-highlighting"),
|
||||||
|
getTime: () => D.getPhase()
|
||||||
|
}), K(() => {
|
||||||
|
if (a) {
|
||||||
|
const F = async (u) => {
|
||||||
|
(u.ctrlKey || u.altKey) && (u.code === "Enter" ? (u.preventDefault(), ae(y), await d()) : u.code === "Period" && (k(), u.preventDefault()));
|
||||||
|
};
|
||||||
|
return window.addEventListener("keydown", F, !0), () => window.removeEventListener("keydown", F, !0);
|
||||||
|
}
|
||||||
|
}, [a, v, s, l, k, y]), /* @__PURE__ */ n.createElement("div", {
|
||||||
|
className: b.container,
|
||||||
|
ref: z
|
||||||
|
}, /* @__PURE__ */ n.createElement("div", {
|
||||||
|
className: b.header
|
||||||
|
}, /* @__PURE__ */ n.createElement("div", {
|
||||||
|
className: b.buttons
|
||||||
|
}, /* @__PURE__ */ n.createElement("button", {
|
||||||
|
className: B(b.button, p ? "sc-animate-pulse" : ""),
|
||||||
|
onClick: () => h()
|
||||||
|
}, /* @__PURE__ */ n.createElement(q, {
|
||||||
|
type: p ? "pause" : "play"
|
||||||
|
})), /* @__PURE__ */ n.createElement("button", {
|
||||||
|
className: B(i ? b.button : b.buttonDisabled),
|
||||||
|
onClick: () => d()
|
||||||
|
}, /* @__PURE__ */ n.createElement(q, {
|
||||||
type: "refresh"
|
type: "refresh"
|
||||||
}))), p && /* @__PURE__ */ y.createElement("div", {
|
}))), c && /* @__PURE__ */ n.createElement("div", {
|
||||||
className: F.error
|
className: b.error
|
||||||
}, p.message)), /* @__PURE__ */ y.createElement("div", {
|
}, c.message)), /* @__PURE__ */ n.createElement("div", {
|
||||||
className: F.body
|
className: b.body
|
||||||
}, L && /* @__PURE__ */ y.createElement(de, {
|
}, H && /* @__PURE__ */ n.createElement(ie, {
|
||||||
value: i,
|
value: s,
|
||||||
onChange: a,
|
onChange: m,
|
||||||
onViewChanged: h
|
onViewChanged: P
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
function Te(e, o, r = 0.05, n = 0.1, s = 0.1) {
|
function ze(e) {
|
||||||
let i = 0, a = 0, t = 10 ** 4, l = 0.01;
|
return R(() => (window.addEventListener("message", e), () => window.removeEventListener("message", e)), [e]), N((t) => window.postMessage(t, "*"), []);
|
||||||
const c = (h) => r = h(r);
|
|
||||||
s = s || n / 2;
|
|
||||||
const f = () => {
|
|
||||||
const h = e(), D = h + n + s;
|
|
||||||
for (a === 0 && (a = h + l); a < D; )
|
|
||||||
a = Math.round(a * t) / t, a >= h && o(a, r, i), a < h && console.log("TOO LATE", a), a += r, i++;
|
|
||||||
};
|
|
||||||
let p;
|
|
||||||
const d = () => {
|
|
||||||
f(), p = setInterval(f, n * 1e3);
|
|
||||||
}, k = () => clearInterval(p);
|
|
||||||
return { setDuration: c, start: d, stop: () => {
|
|
||||||
i = 0, a = 0, k();
|
|
||||||
}, pause: () => k(), duration: r, getPhase: () => a };
|
|
||||||
}
|
|
||||||
class _e {
|
|
||||||
worker;
|
|
||||||
pattern;
|
|
||||||
started = !1;
|
|
||||||
cps = 1;
|
|
||||||
getTime;
|
|
||||||
phase = 0;
|
|
||||||
constructor({ interval: o, onTrigger: r, onError: n, getTime: s, latency: i = 0.1 }) {
|
|
||||||
this.getTime = s;
|
|
||||||
const a = (t) => Math.round(t * 1e3) / 1e3;
|
|
||||||
this.clock = Te(
|
|
||||||
s,
|
|
||||||
(t, l, c) => {
|
|
||||||
c === 0 && (this.origin = t);
|
|
||||||
const f = a(t - this.origin);
|
|
||||||
this.phase = f - i;
|
|
||||||
const p = a(f + l), d = s();
|
|
||||||
try {
|
|
||||||
this.pattern.queryArc(f, p).forEach((g) => {
|
|
||||||
if (g.part.begin.equals(g.whole.begin)) {
|
|
||||||
const C = g.whole.begin + this.origin - d + i, v = g.duration * 1;
|
|
||||||
r?.(g, C, v);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (k) {
|
|
||||||
console.warn("scheduler error", k), n?.(k);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
o
|
|
||||||
);
|
|
||||||
}
|
|
||||||
getPhase() {
|
|
||||||
return this.phase;
|
|
||||||
}
|
|
||||||
start() {
|
|
||||||
if (!this.pattern)
|
|
||||||
throw new Error("Scheduler: no pattern set! call .setPattern first.");
|
|
||||||
this.clock.start(), this.started = !0;
|
|
||||||
}
|
|
||||||
pause() {
|
|
||||||
this.clock.stop(), delete this.origin, this.started = !1;
|
|
||||||
}
|
|
||||||
stop() {
|
|
||||||
delete this.origin, this.clock.stop(), this.started = !1;
|
|
||||||
}
|
|
||||||
setPattern(o) {
|
|
||||||
this.pattern = o;
|
|
||||||
}
|
|
||||||
setCps(o = 1) {
|
|
||||||
this.cps = o;
|
|
||||||
}
|
|
||||||
log(o, r, n) {
|
|
||||||
const s = n.filter((i) => i.hasOnset());
|
|
||||||
console.log(`${o.toFixed(4)} - ${r.toFixed(4)} ${Array(s.length).fill("I").join("")}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function Ve({ defaultOutput: e, interval: o, getTime: r, code: n, evalOnMount: s = !1 }) {
|
|
||||||
const [i, a] = w(), [t, l] = w(), [c, f] = w(n), [p, d] = w(), k = n !== c, g = R(
|
|
||||||
() => new _e({ interval: o, onTrigger: e, onError: a, getTime: r }),
|
|
||||||
[e, o]
|
|
||||||
), C = T(async () => {
|
|
||||||
if (!n) {
|
|
||||||
console.log("no code..");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const { pattern: h } = await G(n);
|
|
||||||
f(n), g?.setPattern(h), d(h), l();
|
|
||||||
} catch (h) {
|
|
||||||
l(h), console.warn("eval error", h);
|
|
||||||
}
|
|
||||||
}, [n, g]), v = I();
|
|
||||||
return q(() => {
|
|
||||||
!v.current && s && (v.current = !0, C());
|
|
||||||
}, [C, s]), { schedulerError: i, scheduler: g, evalError: t, evaluate: C, activeCode: c, isDirty: k, pattern: p };
|
|
||||||
}
|
|
||||||
const $e = (e) => j(() => (window.addEventListener("keydown", e, !0), () => window.removeEventListener("keydown", e, !0)), [e]);
|
|
||||||
function We(e) {
|
|
||||||
const { ready: o, connected: r, disconnected: n } = e, [s, i] = w(!0), [a, t] = w(N?.outputs || []);
|
|
||||||
return q(() => {
|
|
||||||
se().then(() => {
|
|
||||||
N.addListener("connected", (c) => {
|
|
||||||
t([...N.outputs]), r?.(N, c);
|
|
||||||
}), N.addListener("disconnected", (c) => {
|
|
||||||
t([...N.outputs]), n?.(N, c);
|
|
||||||
}), o?.(N), i(!1);
|
|
||||||
}).catch((c) => {
|
|
||||||
if (c) {
|
|
||||||
console.error(c), console.warn("Web Midi could not be enabled..");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, [o, r, n, a]), { loading: s, outputs: a, outputByName: (c) => N.getOutputByName(c) };
|
|
||||||
}
|
}
|
||||||
|
const He = (e) => K(() => (window.addEventListener("keydown", e, !0), () => window.removeEventListener("keydown", e, !0)), [e]);
|
||||||
export {
|
export {
|
||||||
de as CodeMirror,
|
ie as CodeMirror,
|
||||||
Be as MiniRepl,
|
Pe as MiniRepl,
|
||||||
$ as cx,
|
B as cx,
|
||||||
ie as flash,
|
ae as flash,
|
||||||
fe as useCycle,
|
le as useHighlighting,
|
||||||
ye as useHighlighting,
|
He as useKeydown,
|
||||||
$e as useKeydown,
|
ze as usePostMessage,
|
||||||
ge as usePostMessage,
|
ve as useStrudel
|
||||||
pe as useRepl,
|
|
||||||
Ve as useStrudel,
|
|
||||||
We as useWebMidi
|
|
||||||
};
|
};
|
||||||
|
|||||||
2
packages/react/dist/style.css
vendored
2
packages/react/dist/style.css
vendored
@ -1 +1 @@
|
|||||||
.cm-editor{background-color:transparent!important;height:100%;z-index:11;font-size:16px}.cm-theme-light{width:100%}.cm-line>*{background:#00000095}*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.sc-h-5{height:1.25rem}.sc-w-5{width:1.25rem}@keyframes sc-pulse{50%{opacity:.5}}.sc-animate-pulse{animation:sc-pulse 2s cubic-bezier(.4,0,.6,1) infinite}._container_3i85k_1{overflow:hidden;border-radius:.375rem;--tw-bg-opacity: 1;background-color:rgb(34 34 34 / var(--tw-bg-opacity))}._header_3i85k_5{display:flex;justify-content:space-between;border-top-width:1px;--tw-border-opacity: 1;border-color:rgb(100 116 139 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity))}._buttons_3i85k_9{display:flex}._button_3i85k_9{display:flex;width:4rem;cursor:pointer;align-items:center;justify-content:center;border-right-width:1px;--tw-border-opacity: 1;border-color:rgb(100 116 139 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity));padding:.25rem;--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}._button_3i85k_9:hover{--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity))}._buttonDisabled_3i85k_17{display:flex;width:4rem;cursor:pointer;cursor:not-allowed;align-items:center;justify-content:center;--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity));padding:.25rem;--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}._error_3i85k_21{padding:.25rem;text-align:right;font-size:.875rem;line-height:1.25rem;--tw-text-opacity: 1;color:rgb(254 202 202 / var(--tw-text-opacity))}._body_3i85k_25{position:relative;overflow:auto}
|
.cm-editor{background-color:transparent!important;height:100%;z-index:11;font-size:18px}.cm-theme-light{width:100%}.cm-line>*{background:#00000095}*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.sc-h-5{height:1.25rem}.sc-w-5{width:1.25rem}@keyframes sc-pulse{50%{opacity:.5}}.sc-animate-pulse{animation:sc-pulse 2s cubic-bezier(.4,0,.6,1) infinite}._container_3i85k_1{overflow:hidden;border-radius:.375rem;--tw-bg-opacity: 1;background-color:rgb(34 34 34 / var(--tw-bg-opacity))}._header_3i85k_5{display:flex;justify-content:space-between;border-top-width:1px;--tw-border-opacity: 1;border-color:rgb(100 116 139 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity))}._buttons_3i85k_9{display:flex}._button_3i85k_9{display:flex;width:4rem;cursor:pointer;align-items:center;justify-content:center;border-right-width:1px;--tw-border-opacity: 1;border-color:rgb(100 116 139 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity));padding:.25rem;--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}._button_3i85k_9:hover{--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity))}._buttonDisabled_3i85k_17{display:flex;width:4rem;cursor:pointer;cursor:not-allowed;align-items:center;justify-content:center;--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity));padding:.25rem;--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}._error_3i85k_21{padding:.25rem;text-align:right;font-size:.875rem;line-height:1.25rem;--tw-text-opacity: 1;color:rgb(254 202 202 / var(--tw-text-opacity))}._body_3i85k_25{position:relative;overflow:auto}
|
||||||
|
|||||||
@ -82,7 +82,7 @@ function App() {
|
|||||||
view,
|
view,
|
||||||
pattern,
|
pattern,
|
||||||
active: !activeCode?.includes('strudel disable-highlighting'),
|
active: !activeCode?.includes('strudel disable-highlighting'),
|
||||||
getTime: () => scheduler.phase,
|
getTime: () => scheduler.getPhase(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const error = evalError || schedulerError;
|
const error = evalError || schedulerError;
|
||||||
|
|||||||
@ -17,6 +17,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
|
"watch": "vite build --watch",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
@ -39,7 +40,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/lang-javascript": "^6.1.1",
|
"@codemirror/lang-javascript": "^6.1.1",
|
||||||
"@strudel.cycles/core": "^0.3.2",
|
"@strudel.cycles/core": "^0.3.2",
|
||||||
"@strudel.cycles/eval": "^0.3.2",
|
"@strudel.cycles/transpiler": "^0.3.2",
|
||||||
|
"@strudel.cycles/webaudio": "^0.3.3",
|
||||||
"@strudel.cycles/tone": "^0.3.3",
|
"@strudel.cycles/tone": "^0.3.3",
|
||||||
"@uiw/codemirror-themes": "^4.12.4",
|
"@uiw/codemirror-themes": "^4.12.4",
|
||||||
"@uiw/react-codemirror": "^4.12.4",
|
"@uiw/react-codemirror": "^4.12.4",
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import { controls, evalScope } from '@strudel.cycles/core';
|
|||||||
evalScope(
|
evalScope(
|
||||||
controls,
|
controls,
|
||||||
import('@strudel.cycles/core'),
|
import('@strudel.cycles/core'),
|
||||||
import('@strudel.cycles/tone'),
|
// import('@strudel.cycles/tone'),
|
||||||
import('@strudel.cycles/tonal'),
|
import('@strudel.cycles/tonal'),
|
||||||
import('@strudel.cycles/mini'),
|
import('@strudel.cycles/mini'),
|
||||||
import('@strudel.cycles/midi'),
|
import('@strudel.cycles/midi'),
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import React, { useState, useMemo, useRef, useEffect, useLayoutEffect } from 'react';
|
import React, { useState, useMemo, useRef, useEffect, useLayoutEffect } from 'react';
|
||||||
import { useInView } from 'react-hook-inview';
|
import { useInView } from 'react-hook-inview';
|
||||||
import useRepl from '../hooks/useRepl.mjs';
|
|
||||||
import cx from '../cx';
|
import cx from '../cx';
|
||||||
import useHighlighting from '../hooks/useHighlighting.mjs';
|
import useHighlighting from '../hooks/useHighlighting.mjs';
|
||||||
import CodeMirror6, { flash } from './CodeMirror6';
|
import CodeMirror6, { flash } from './CodeMirror6';
|
||||||
@ -8,18 +7,33 @@ import 'tailwindcss/tailwind.css';
|
|||||||
import './style.css';
|
import './style.css';
|
||||||
import styles from './MiniRepl.module.css';
|
import styles from './MiniRepl.module.css';
|
||||||
import { Icon } from './Icon';
|
import { Icon } from './Icon';
|
||||||
import { Tone } from '@strudel.cycles/tone';
|
import { getAudioContext, webaudioOutput } from '@strudel.cycles/webaudio';
|
||||||
|
import useStrudel from '../hooks/useStrudel.mjs';
|
||||||
|
|
||||||
export function MiniRepl({ tune, hideOutsideView = false, init, onEvent, enableKeyboard }) {
|
const getTime = () => getAudioContext().currentTime;
|
||||||
const { code, setCode, pattern, activeCode, activateCode, evaluateOnly, error, cycle, dirty, togglePlay, stop } =
|
|
||||||
useRepl({
|
export function MiniRepl({ tune, hideOutsideView = false, init, enableKeyboard }) {
|
||||||
tune,
|
const {
|
||||||
autolink: false,
|
code,
|
||||||
onEvent,
|
setCode,
|
||||||
});
|
evaluate,
|
||||||
useEffect(() => {
|
activateCode,
|
||||||
init && evaluateOnly();
|
error,
|
||||||
}, [tune, init]);
|
isDirty,
|
||||||
|
activeCode,
|
||||||
|
pattern,
|
||||||
|
started,
|
||||||
|
scheduler,
|
||||||
|
togglePlay,
|
||||||
|
stop,
|
||||||
|
} = useStrudel({
|
||||||
|
initialCode: tune,
|
||||||
|
defaultOutput: webaudioOutput,
|
||||||
|
getTime,
|
||||||
|
});
|
||||||
|
/* useEffect(() => {
|
||||||
|
init && activateCode();
|
||||||
|
}, [init, activateCode]); */
|
||||||
const [view, setView] = useState();
|
const [view, setView] = useState();
|
||||||
const [ref, isVisible] = useInView({
|
const [ref, isVisible] = useInView({
|
||||||
threshold: 0.01,
|
threshold: 0.01,
|
||||||
@ -34,8 +48,8 @@ export function MiniRepl({ tune, hideOutsideView = false, init, onEvent, enableK
|
|||||||
useHighlighting({
|
useHighlighting({
|
||||||
view,
|
view,
|
||||||
pattern,
|
pattern,
|
||||||
active: cycle.started && !activeCode?.includes('strudel disable-highlighting'),
|
active: started && !activeCode?.includes('strudel disable-highlighting'),
|
||||||
getTime: () => Tone.getTransport().seconds,
|
getTime: () => scheduler.getPhase(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// set active pattern on ctrl+enter
|
// set active pattern on ctrl+enter
|
||||||
@ -48,7 +62,7 @@ export function MiniRepl({ tune, hideOutsideView = false, init, onEvent, enableK
|
|||||||
flash(view);
|
flash(view);
|
||||||
await activateCode();
|
await activateCode();
|
||||||
} else if (e.code === 'Period') {
|
} else if (e.code === 'Period') {
|
||||||
cycle.stop();
|
stop();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -56,16 +70,16 @@ export function MiniRepl({ tune, hideOutsideView = false, init, onEvent, enableK
|
|||||||
window.addEventListener('keydown', handleKeyPress, true);
|
window.addEventListener('keydown', handleKeyPress, true);
|
||||||
return () => window.removeEventListener('keydown', handleKeyPress, true);
|
return () => window.removeEventListener('keydown', handleKeyPress, true);
|
||||||
}
|
}
|
||||||
}, [enableKeyboard, pattern, code, activateCode, cycle, view]);
|
}, [enableKeyboard, pattern, code, evaluate, stop, view]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container} ref={ref}>
|
<div className={styles.container} ref={ref}>
|
||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
<div className={styles.buttons}>
|
<div className={styles.buttons}>
|
||||||
<button className={cx(styles.button, cycle.started ? 'sc-animate-pulse' : '')} onClick={() => togglePlay()}>
|
<button className={cx(styles.button, started ? 'sc-animate-pulse' : '')} onClick={() => togglePlay()}>
|
||||||
<Icon type={cycle.started ? 'pause' : 'play'} />
|
<Icon type={started ? 'pause' : 'play'} />
|
||||||
</button>
|
</button>
|
||||||
<button className={cx(dirty ? styles.button : styles.buttonDisabled)} onClick={() => activateCode()}>
|
<button className={cx(isDirty ? styles.button : styles.buttonDisabled)} onClick={() => activateCode()}>
|
||||||
<Icon type="refresh" />
|
<Icon type="refresh" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
z-index: 11;
|
z-index: 11;
|
||||||
font-size: 16px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cm-theme-light {
|
.cm-theme-light {
|
||||||
|
|||||||
@ -1,86 +0,0 @@
|
|||||||
/*
|
|
||||||
useCycle.mjs - <short description TODO>
|
|
||||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/useCycle.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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { Tone } from '@strudel.cycles/tone';
|
|
||||||
import { State, TimeSpan } from '@strudel.cycles/core';
|
|
||||||
|
|
||||||
/* export declare interface UseCycleProps {
|
|
||||||
onEvent: ToneEventCallback<any>;
|
|
||||||
onQuery?: (state: State) => Hap[];
|
|
||||||
onSchedule?: (events: Hap[], cycle: number) => void;
|
|
||||||
onDraw?: ToneEventCallback<any>;
|
|
||||||
ready?: boolean; // if false, query will not be called on change props
|
|
||||||
} */
|
|
||||||
|
|
||||||
// function useCycle(props: UseCycleProps) {
|
|
||||||
function useCycle(props) {
|
|
||||||
// onX must use useCallback!
|
|
||||||
const { onEvent, onQuery, onSchedule, ready = true, onDraw } = props;
|
|
||||||
const [started, setStarted] = useState(false);
|
|
||||||
const cycleDuration = 1;
|
|
||||||
const activeCycle = () => Math.floor(Tone.getTransport().seconds / cycleDuration);
|
|
||||||
|
|
||||||
// pull events with onQuery + count up to next cycle
|
|
||||||
const query = (cycle = activeCycle()) => {
|
|
||||||
const timespan = new TimeSpan(cycle, cycle + 1);
|
|
||||||
const events = onQuery?.(new State(timespan)) || [];
|
|
||||||
onSchedule?.(events, cycle);
|
|
||||||
// cancel events after current query. makes sure no old events are player for rescheduled cycles
|
|
||||||
// console.log('schedule', cycle);
|
|
||||||
// query next cycle in the middle of the current
|
|
||||||
const cancelFrom = timespan.begin.valueOf();
|
|
||||||
Tone.getTransport().cancel(cancelFrom);
|
|
||||||
// const queryNextTime = (cycle + 1) * cycleDuration - 0.1;
|
|
||||||
const queryNextTime = (cycle + 1) * cycleDuration - 0.5;
|
|
||||||
|
|
||||||
// if queryNextTime would be before current time, execute directly (+0.1 for safety that it won't miss)
|
|
||||||
const t = Math.max(Tone.getTransport().seconds, queryNextTime) + 0.1;
|
|
||||||
Tone.getTransport().schedule(() => {
|
|
||||||
query(cycle + 1);
|
|
||||||
}, t);
|
|
||||||
|
|
||||||
// schedule events for next cycle
|
|
||||||
events
|
|
||||||
?.filter((event) => event.part.begin.equals(event.whole?.begin))
|
|
||||||
.forEach((event) => {
|
|
||||||
Tone.getTransport().schedule((time) => {
|
|
||||||
onEvent(time, event, Tone.getContext().currentTime);
|
|
||||||
Tone.Draw.schedule(() => {
|
|
||||||
// do drawing or DOM manipulation here
|
|
||||||
onDraw?.(time, event);
|
|
||||||
}, time);
|
|
||||||
}, event.part.begin.valueOf());
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
ready && query();
|
|
||||||
}, [onEvent, onSchedule, onQuery, onDraw, ready]);
|
|
||||||
|
|
||||||
const start = async () => {
|
|
||||||
setStarted(true);
|
|
||||||
await Tone.start();
|
|
||||||
Tone.getTransport().start('+0.1');
|
|
||||||
};
|
|
||||||
const stop = () => {
|
|
||||||
Tone.getTransport().pause();
|
|
||||||
setStarted(false);
|
|
||||||
};
|
|
||||||
const toggle = () => (started ? stop() : start());
|
|
||||||
return {
|
|
||||||
start,
|
|
||||||
stop,
|
|
||||||
onEvent,
|
|
||||||
started,
|
|
||||||
setStarted,
|
|
||||||
toggle,
|
|
||||||
query,
|
|
||||||
activeCycle,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default useCycle;
|
|
||||||
@ -14,15 +14,14 @@ function useHighlighting({ view, pattern, active, getTime }) {
|
|||||||
const audioTime = getTime();
|
const audioTime = getTime();
|
||||||
// force min framerate of 10 fps => fixes crash on tab refocus, where lastEnd could be far away
|
// force min framerate of 10 fps => fixes crash on tab refocus, where lastEnd could be far away
|
||||||
// see https://github.com/tidalcycles/strudel/issues/108
|
// see https://github.com/tidalcycles/strudel/issues/108
|
||||||
const begin = Math.max(lastEnd.current || audioTime, audioTime - 1 / 10);
|
const begin = Math.max(lastEnd.current || audioTime, audioTime - 1 / 10, 0); // negative time seems buggy
|
||||||
const span = [begin, audioTime + 1 / 60];
|
const span = [begin, audioTime + 1 / 60];
|
||||||
lastEnd.current = audioTime + 1 / 60;
|
lastEnd.current = span[1];
|
||||||
highlights.current = highlights.current.filter((hap) => hap.whole.end > audioTime); // keep only highlights that are still active
|
highlights.current = highlights.current.filter((hap) => hap.whole.end > audioTime); // keep only highlights that are still active
|
||||||
const haps = pattern.queryArc(...span).filter((hap) => hap.hasOnset());
|
const haps = pattern.queryArc(...span).filter((hap) => hap.hasOnset());
|
||||||
highlights.current = highlights.current.concat(haps); // add potential new onsets
|
highlights.current = highlights.current.concat(haps); // add potential new onsets
|
||||||
view.dispatch({ effects: setHighlights.of(highlights.current) }); // highlight all still active + new active haps
|
view.dispatch({ effects: setHighlights.of(highlights.current) }); // highlight all still active + new active haps
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// console.log('error in updateHighlights', err);
|
|
||||||
view.dispatch({ effects: setHighlights.of([]) });
|
view.dispatch({ effects: setHighlights.of([]) });
|
||||||
}
|
}
|
||||||
frame = requestAnimationFrame(updateHighlights);
|
frame = requestAnimationFrame(updateHighlights);
|
||||||
|
|||||||
@ -1,150 +0,0 @@
|
|||||||
/*
|
|
||||||
useRepl.mjs - <short description TODO>
|
|
||||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/useRepl.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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { useCallback, useState, useMemo } from 'react';
|
|
||||||
import { evaluate } from '@strudel.cycles/eval';
|
|
||||||
import useCycle from './useCycle.mjs';
|
|
||||||
import usePostMessage from './usePostMessage.mjs';
|
|
||||||
import { webaudioOutputTrigger } from '@strudel.cycles/webaudio';
|
|
||||||
|
|
||||||
let s4 = () => {
|
|
||||||
return Math.floor((1 + Math.random()) * 0x10000)
|
|
||||||
.toString(16)
|
|
||||||
.substring(1);
|
|
||||||
};
|
|
||||||
const generateHash = (code) => encodeURIComponent(btoa(code));
|
|
||||||
|
|
||||||
function useRepl({ tune, autolink = true, onEvent, onDraw: onDrawProp }) {
|
|
||||||
const id = useMemo(() => s4(), []);
|
|
||||||
const [code, setCode] = useState(tune);
|
|
||||||
const [activeCode, setActiveCode] = useState();
|
|
||||||
const [log, setLog] = useState('');
|
|
||||||
const [error, setError] = useState();
|
|
||||||
const [pending, setPending] = useState(false);
|
|
||||||
const [hash, setHash] = useState('');
|
|
||||||
const [pattern, setPattern] = useState();
|
|
||||||
const dirty = useMemo(() => code !== activeCode || error, [code, activeCode, error]);
|
|
||||||
const pushLog = useCallback((message) => setLog((log) => log + `${log ? '\n\n' : ''}${message}`), []);
|
|
||||||
|
|
||||||
// below block allows disabling the highlighting by including "strudel disable-highlighting" in the code (as comment)
|
|
||||||
const onDraw = useMemo(() => {
|
|
||||||
if (activeCode && !activeCode.includes('strudel disable-highlighting')) {
|
|
||||||
return (time, event) => onDrawProp?.(time, event, activeCode);
|
|
||||||
}
|
|
||||||
}, [activeCode, onDrawProp]);
|
|
||||||
|
|
||||||
const hideHeader = useMemo(() => activeCode && activeCode.includes('strudel hide-header'), [activeCode]);
|
|
||||||
const hideConsole = useMemo(() => activeCode && activeCode.includes('strudel hide-console'), [activeCode]);
|
|
||||||
// cycle hook to control scheduling
|
|
||||||
const cycle = useCycle({
|
|
||||||
onDraw,
|
|
||||||
onEvent: useCallback(
|
|
||||||
(time, event, currentTime) => {
|
|
||||||
try {
|
|
||||||
onEvent?.(event);
|
|
||||||
if (event.context.logs?.length) {
|
|
||||||
event.context.logs.forEach(pushLog);
|
|
||||||
}
|
|
||||||
const { onTrigger = webaudioOutputTrigger } = event.context;
|
|
||||||
onTrigger(time, event, currentTime, 1 /* cps */);
|
|
||||||
} catch (err) {
|
|
||||||
console.warn(err);
|
|
||||||
err.message = 'unplayable event: ' + err?.message;
|
|
||||||
pushLog(err.message); // not with setError, because then we would have to setError(undefined) on next playable event
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[onEvent, pushLog],
|
|
||||||
),
|
|
||||||
onQuery: useCallback(
|
|
||||||
(state) => {
|
|
||||||
try {
|
|
||||||
return pattern?.query(state) || [];
|
|
||||||
} catch (err) {
|
|
||||||
console.warn(err);
|
|
||||||
err.message = 'query error: ' + err.message;
|
|
||||||
setError(err);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[pattern],
|
|
||||||
),
|
|
||||||
onSchedule: useCallback((_events, cycle) => logCycle(_events, cycle), []),
|
|
||||||
ready: !!pattern && !!activeCode,
|
|
||||||
});
|
|
||||||
|
|
||||||
const broadcast = usePostMessage(({ data: { from, type } }) => {
|
|
||||||
if (type === 'start' && from !== id) {
|
|
||||||
// console.log('message', from, type);
|
|
||||||
cycle.setStarted(false);
|
|
||||||
setActiveCode(undefined);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const activateCode = useCallback(
|
|
||||||
async (_code = code) => {
|
|
||||||
if (activeCode && !dirty) {
|
|
||||||
setError(undefined);
|
|
||||||
cycle.start();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
setPending(true);
|
|
||||||
const parsed = await evaluate(_code);
|
|
||||||
cycle.start();
|
|
||||||
broadcast({ type: 'start', from: id });
|
|
||||||
setPattern(() => parsed.pattern);
|
|
||||||
if (autolink) {
|
|
||||||
window.location.hash = '#' + encodeURIComponent(btoa(code));
|
|
||||||
}
|
|
||||||
setHash(generateHash(code));
|
|
||||||
setError(undefined);
|
|
||||||
setActiveCode(_code);
|
|
||||||
setPending(false);
|
|
||||||
} catch (err) {
|
|
||||||
err.message = 'evaluation error: ' + err.message;
|
|
||||||
console.warn(err);
|
|
||||||
setError(err);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[activeCode, dirty, code, cycle, autolink, id, broadcast],
|
|
||||||
);
|
|
||||||
// logs events of cycle
|
|
||||||
const logCycle = (_events, cycle) => {
|
|
||||||
if (_events.length) {
|
|
||||||
// pushLog(`# cycle ${cycle}\n` + _events.map((e: any) => e.show()).join('\n'));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const togglePlay = () => {
|
|
||||||
if (!cycle.started) {
|
|
||||||
activateCode();
|
|
||||||
} else {
|
|
||||||
cycle.stop();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
hideHeader,
|
|
||||||
hideConsole,
|
|
||||||
pending,
|
|
||||||
code,
|
|
||||||
setCode,
|
|
||||||
pattern,
|
|
||||||
error,
|
|
||||||
cycle,
|
|
||||||
setPattern,
|
|
||||||
dirty,
|
|
||||||
log,
|
|
||||||
togglePlay,
|
|
||||||
setActiveCode,
|
|
||||||
activateCode,
|
|
||||||
activeCode,
|
|
||||||
pushLog,
|
|
||||||
hash,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default useRepl;
|
|
||||||
@ -1,43 +1,105 @@
|
|||||||
import { useRef, useCallback, useEffect, useMemo, useState } from 'react';
|
import { useRef, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { repl } from '@strudel.cycles/core/repl.mjs';
|
import { repl } from '@strudel.cycles/core';
|
||||||
import { transpiler } from '@strudel.cycles/transpiler';
|
import { transpiler } from '@strudel.cycles/transpiler';
|
||||||
|
|
||||||
function useStrudel({ defaultOutput, interval, getTime, code, evalOnMount = false }) {
|
function useStrudel({
|
||||||
|
defaultOutput,
|
||||||
|
interval,
|
||||||
|
getTime,
|
||||||
|
evalOnMount = false,
|
||||||
|
initialCode = '',
|
||||||
|
autolink = false,
|
||||||
|
beforeEval,
|
||||||
|
afterEval,
|
||||||
|
onEvalError,
|
||||||
|
onToggle,
|
||||||
|
}) {
|
||||||
// scheduler
|
// scheduler
|
||||||
const [schedulerError, setSchedulerError] = useState();
|
const [schedulerError, setSchedulerError] = useState();
|
||||||
const [evalError, setEvalError] = useState();
|
const [evalError, setEvalError] = useState();
|
||||||
|
const [code, setCode] = useState(initialCode);
|
||||||
const [activeCode, setActiveCode] = useState(code);
|
const [activeCode, setActiveCode] = useState(code);
|
||||||
const [pattern, setPattern] = useState();
|
const [pattern, setPattern] = useState();
|
||||||
|
const [started, setStarted] = useState(false);
|
||||||
const isDirty = code !== activeCode;
|
const isDirty = code !== activeCode;
|
||||||
const { scheduler, evaluate: _evaluate } = useMemo(
|
|
||||||
|
// TODO: make sure this hook reruns when scheduler.started changes
|
||||||
|
const { scheduler, evaluate, start, stop, pause } = useMemo(
|
||||||
() =>
|
() =>
|
||||||
repl({
|
repl({
|
||||||
interval,
|
interval,
|
||||||
defaultOutput,
|
defaultOutput,
|
||||||
onSchedulerError: setSchedulerError,
|
onSchedulerError: setSchedulerError,
|
||||||
onEvalError: setEvalError,
|
onEvalError: (err) => {
|
||||||
|
setEvalError(err);
|
||||||
|
onEvalError?.(err);
|
||||||
|
},
|
||||||
getTime,
|
getTime,
|
||||||
transpiler,
|
transpiler,
|
||||||
onEval: ({ pattern: _pattern, code }) => {
|
beforeEval: ({ code }) => {
|
||||||
|
setCode(code);
|
||||||
|
beforeEval?.();
|
||||||
|
},
|
||||||
|
afterEval: ({ pattern: _pattern, code }) => {
|
||||||
setActiveCode(code);
|
setActiveCode(code);
|
||||||
setPattern(_pattern);
|
setPattern(_pattern);
|
||||||
setEvalError();
|
setEvalError();
|
||||||
|
setSchedulerError();
|
||||||
|
if (autolink) {
|
||||||
|
window.location.hash = '#' + encodeURIComponent(btoa(code));
|
||||||
|
}
|
||||||
|
afterEval?.();
|
||||||
|
},
|
||||||
|
onToggle: (v) => {
|
||||||
|
setStarted(v);
|
||||||
|
onToggle?.(v);
|
||||||
},
|
},
|
||||||
onEvalError: setEvalError,
|
|
||||||
}),
|
}),
|
||||||
[defaultOutput, interval, getTime],
|
[defaultOutput, interval, getTime],
|
||||||
);
|
);
|
||||||
const evaluate = useCallback(() => _evaluate(code), [_evaluate, code]);
|
const activateCode = useCallback(async (autostart = true) => evaluate(code, autostart), [evaluate, code]);
|
||||||
|
|
||||||
const inited = useRef();
|
const inited = useRef();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!inited.current && evalOnMount && code) {
|
if (!inited.current && evalOnMount && code) {
|
||||||
inited.current = true;
|
inited.current = true;
|
||||||
evaluate();
|
activateCode();
|
||||||
}
|
}
|
||||||
}, [evaluate, evalOnMount, code]);
|
}, [activateCode, evalOnMount, code]);
|
||||||
|
|
||||||
return { schedulerError, scheduler, evalError, evaluate, activeCode, isDirty, pattern };
|
// this will stop the scheduler when hot reloading in development
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
scheduler.stop();
|
||||||
|
};
|
||||||
|
}, [scheduler]);
|
||||||
|
|
||||||
|
const togglePlay = async () => {
|
||||||
|
if (started) {
|
||||||
|
scheduler.pause();
|
||||||
|
} else {
|
||||||
|
await activateCode();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const error = schedulerError || evalError;
|
||||||
|
return {
|
||||||
|
code,
|
||||||
|
setCode,
|
||||||
|
error,
|
||||||
|
schedulerError,
|
||||||
|
scheduler,
|
||||||
|
evalError,
|
||||||
|
evaluate,
|
||||||
|
activateCode,
|
||||||
|
activeCode,
|
||||||
|
isDirty,
|
||||||
|
pattern,
|
||||||
|
started,
|
||||||
|
start,
|
||||||
|
stop,
|
||||||
|
pause,
|
||||||
|
togglePlay,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default useStrudel;
|
export default useStrudel;
|
||||||
|
|||||||
@ -1,41 +0,0 @@
|
|||||||
/*
|
|
||||||
useWebMidi.js - <short description TODO>
|
|
||||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/useWebMidi.js>
|
|
||||||
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 { useEffect, useState } from 'react';
|
|
||||||
import { enableWebMidi, WebMidi } from '@strudel.cycles/midi'
|
|
||||||
|
|
||||||
export function useWebMidi(props) {
|
|
||||||
const { ready, connected, disconnected } = props;
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [outputs, setOutputs] = useState(WebMidi?.outputs || []);
|
|
||||||
useEffect(() => {
|
|
||||||
enableWebMidi()
|
|
||||||
.then(() => {
|
|
||||||
// Reacting when a new device becomes available
|
|
||||||
WebMidi.addListener('connected', (e) => {
|
|
||||||
setOutputs([...WebMidi.outputs]);
|
|
||||||
connected?.(WebMidi, e);
|
|
||||||
});
|
|
||||||
// Reacting when a device becomes unavailable
|
|
||||||
WebMidi.addListener('disconnected', (e) => {
|
|
||||||
setOutputs([...WebMidi.outputs]);
|
|
||||||
disconnected?.(WebMidi, e);
|
|
||||||
});
|
|
||||||
ready?.(WebMidi);
|
|
||||||
setLoading(false);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
if (err) {
|
|
||||||
console.error(err);
|
|
||||||
//throw new Error("Web Midi could not be enabled...");
|
|
||||||
console.warn('Web Midi could not be enabled..');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, [ready, connected, disconnected, outputs]);
|
|
||||||
const outputByName = (name) => WebMidi.getOutputByName(name);
|
|
||||||
return { loading, outputs, outputByName };
|
|
||||||
}
|
|
||||||
@ -2,11 +2,8 @@
|
|||||||
|
|
||||||
export { default as CodeMirror, flash } from './components/CodeMirror6';
|
export { default as CodeMirror, flash } from './components/CodeMirror6';
|
||||||
export * from './components/MiniRepl';
|
export * from './components/MiniRepl';
|
||||||
export { default as useCycle } from './hooks/useCycle';
|
|
||||||
export { default as useHighlighting } from './hooks/useHighlighting';
|
export { default as useHighlighting } from './hooks/useHighlighting';
|
||||||
export { default as usePostMessage } from './hooks/usePostMessage';
|
export { default as usePostMessage } from './hooks/usePostMessage';
|
||||||
export { default as useRepl } from './hooks/useRepl';
|
|
||||||
export { default as useStrudel } from './hooks/useStrudel';
|
export { default as useStrudel } from './hooks/useStrudel';
|
||||||
export { default as useKeydown } from './hooks/useKeydown';
|
export { default as useKeydown } from './hooks/useKeydown';
|
||||||
export { default as cx } from './cx';
|
export { default as cx } from './cx';
|
||||||
export { useWebMidi } from './hooks/useWebMidi';
|
|
||||||
|
|||||||
@ -8,28 +8,36 @@ export default createTheme({
|
|||||||
caret: '#ffcc00',
|
caret: '#ffcc00',
|
||||||
selection: 'rgba(128, 203, 196, 0.5)',
|
selection: 'rgba(128, 203, 196, 0.5)',
|
||||||
selectionMatch: '#036dd626',
|
selectionMatch: '#036dd626',
|
||||||
lineHighlight: '#8a91991a',
|
// lineHighlight: '#8a91991a', // original
|
||||||
|
lineHighlight: '#00000050',
|
||||||
gutterBackground: 'transparent',
|
gutterBackground: 'transparent',
|
||||||
// gutterForeground: '#8a919966',
|
// gutterForeground: '#8a919966',
|
||||||
gutterForeground: '#676e95',
|
gutterForeground: '#8a919966',
|
||||||
},
|
},
|
||||||
styles: [
|
styles: [
|
||||||
{ tag: t.keyword, color: '#c792ea' },
|
{ tag: t.keyword, color: '#c792ea' },
|
||||||
{ tag: t.operator, color: '#89ddff' },
|
{ tag: t.operator, color: '#89ddff' },
|
||||||
{ tag: t.special(t.variableName), color: '#eeffff' },
|
{ tag: t.special(t.variableName), color: '#eeffff' },
|
||||||
{ tag: t.typeName, color: '#f07178' },
|
// { tag: t.typeName, color: '#f07178' }, // original
|
||||||
|
{ tag: t.typeName, color: '#c3e88d' },
|
||||||
{ tag: t.atom, color: '#f78c6c' },
|
{ tag: t.atom, color: '#f78c6c' },
|
||||||
{ tag: t.number, color: '#ff5370' },
|
// { tag: t.number, color: '#ff5370' }, // original
|
||||||
|
{ tag: t.number, color: '#c3e88d' },
|
||||||
{ tag: t.definition(t.variableName), color: '#82aaff' },
|
{ tag: t.definition(t.variableName), color: '#82aaff' },
|
||||||
{ tag: t.string, color: '#c3e88d' },
|
{ tag: t.string, color: '#c3e88d' },
|
||||||
{ tag: t.special(t.string), color: '#f07178' },
|
// { tag: t.special(t.string), color: '#f07178' }, // original
|
||||||
|
{ tag: t.special(t.string), color: '#c3e88d' },
|
||||||
{ tag: t.comment, color: '#7d8799' },
|
{ tag: t.comment, color: '#7d8799' },
|
||||||
{ tag: t.variableName, color: '#f07178' },
|
// { tag: t.variableName, color: '#f07178' }, // original
|
||||||
{ tag: t.tagName, color: '#ff5370' },
|
{ tag: t.variableName, color: '#c792ea' },
|
||||||
{ tag: t.bracket, color: '#a2a1a4' },
|
// { tag: t.tagName, color: '#ff5370' }, // original
|
||||||
|
{ tag: t.tagName, color: '#c3e88d' },
|
||||||
|
{ tag: t.bracket, color: '#525154' },
|
||||||
|
// { tag: t.bracket, color: '#a2a1a4' }, // original
|
||||||
{ tag: t.meta, color: '#ffcb6b' },
|
{ tag: t.meta, color: '#ffcb6b' },
|
||||||
{ tag: t.attributeName, color: '#c792ea' },
|
{ tag: t.attributeName, color: '#c792ea' },
|
||||||
{ tag: t.propertyName, color: '#c792ea' },
|
{ tag: t.propertyName, color: '#c792ea' },
|
||||||
|
|
||||||
{ tag: t.className, color: '#decb6b' },
|
{ tag: t.className, color: '#decb6b' },
|
||||||
{ tag: t.invalid, color: '#ffffff' },
|
{ tag: t.invalid, color: '#ffffff' },
|
||||||
],
|
],
|
||||||
|
|||||||
@ -24,8 +24,9 @@ export default defineConfig({
|
|||||||
// TODO: find out which of below names are obsolete now
|
// TODO: find out which of below names are obsolete now
|
||||||
'@strudel.cycles/tone',
|
'@strudel.cycles/tone',
|
||||||
'@strudel.cycles/eval',
|
'@strudel.cycles/eval',
|
||||||
|
'@strudel.cycles/transpiler',
|
||||||
|
'acorn',
|
||||||
'@strudel.cycles/core',
|
'@strudel.cycles/core',
|
||||||
'@strudel.cycles/core/util.mjs',
|
|
||||||
'@strudel.cycles/mini',
|
'@strudel.cycles/mini',
|
||||||
'@strudel.cycles/tonal',
|
'@strudel.cycles/tonal',
|
||||||
'@strudel.cycles/midi',
|
'@strudel.cycles/midi',
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import { Pattern, isPattern } from '@strudel.cycles/core';
|
|||||||
var serialWriter;
|
var serialWriter;
|
||||||
var choosing = false;
|
var choosing = false;
|
||||||
|
|
||||||
export async function getWriter(br=38400) {
|
export async function getWriter(br = 38400) {
|
||||||
if (choosing) {
|
if (choosing) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -24,11 +24,10 @@ export async function getWriter(br=38400) {
|
|||||||
const writableStreamClosed = textEncoder.readable.pipeTo(port.writable);
|
const writableStreamClosed = textEncoder.readable.pipeTo(port.writable);
|
||||||
const writer = textEncoder.writable.getWriter();
|
const writer = textEncoder.writable.getWriter();
|
||||||
serialWriter = function (message) {
|
serialWriter = function (message) {
|
||||||
writer.write(message)
|
writer.write(message);
|
||||||
}
|
};
|
||||||
}
|
} else {
|
||||||
else {
|
throw 'Webserial is not available in this browser.';
|
||||||
throw('Webserial is not available in this browser.')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,7 +39,7 @@ Pattern.prototype.serial = function (...args) {
|
|||||||
getWriter(...args);
|
getWriter(...args);
|
||||||
}
|
}
|
||||||
const onTrigger = (time, hap, currentTime) => {
|
const onTrigger = (time, hap, currentTime) => {
|
||||||
var message = "";
|
var message = '';
|
||||||
if (typeof hap.value === 'object') {
|
if (typeof hap.value === 'object') {
|
||||||
if ('action' in hap.value) {
|
if ('action' in hap.value) {
|
||||||
message += hap.value['action'] + '(';
|
message += hap.value['action'] + '(';
|
||||||
@ -51,26 +50,23 @@ Pattern.prototype.serial = function (...args) {
|
|||||||
}
|
}
|
||||||
if (first) {
|
if (first) {
|
||||||
first = false;
|
first = false;
|
||||||
|
} else {
|
||||||
|
message += ',';
|
||||||
}
|
}
|
||||||
else {
|
message += `${key}:${val}`;
|
||||||
message +=',';
|
|
||||||
}
|
|
||||||
message += `${key}:${val}`
|
|
||||||
}
|
}
|
||||||
message += ')';
|
message += ')';
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
for (const [key, val] of Object.entries(hap.value)) {
|
for (const [key, val] of Object.entries(hap.value)) {
|
||||||
message += `${key}:${val};`
|
message += `${key}:${val};`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
message = hap.value;
|
message = hap.value;
|
||||||
}
|
}
|
||||||
const offset = (time - currentTime + latency) * 1000;
|
const offset = (time - currentTime + latency) * 1000;
|
||||||
window.setTimeout(serialWriter, offset, message);
|
window.setTimeout(serialWriter, offset, message);
|
||||||
};
|
};
|
||||||
return hap.setContext({ ...hap.context, onTrigger });
|
return hap.setContext({ ...hap.context, onTrigger, dominantTrigger: true });
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,5 +1 @@
|
|||||||
import './pianoroll.mjs';
|
|
||||||
|
|
||||||
export * from './tone.mjs';
|
export * from './tone.mjs';
|
||||||
export * from './draw.mjs';
|
|
||||||
export * from './ui.mjs';
|
|
||||||
|
|||||||
@ -50,32 +50,29 @@ export const getDefaultSynth = () => {
|
|||||||
|
|
||||||
// with this function, you can play the pattern with any tone synth
|
// with this function, you can play the pattern with any tone synth
|
||||||
Pattern.prototype.tone = function (instrument) {
|
Pattern.prototype.tone = function (instrument) {
|
||||||
return this._withHap((hap) => {
|
return this.onTrigger((time, hap) => {
|
||||||
const onTrigger = (time, hap) => {
|
let note;
|
||||||
let note;
|
let velocity = hap.context?.velocity ?? 0.75;
|
||||||
let velocity = hap.context?.velocity ?? 0.75;
|
if (instrument instanceof PluckSynth) {
|
||||||
if (instrument instanceof PluckSynth) {
|
note = getPlayableNoteValue(hap);
|
||||||
note = getPlayableNoteValue(hap);
|
instrument.triggerAttack(note, time);
|
||||||
instrument.triggerAttack(note, time);
|
} else if (instrument instanceof NoiseSynth) {
|
||||||
} else if (instrument instanceof NoiseSynth) {
|
instrument.triggerAttackRelease(hap.duration.valueOf(), time); // noise has no value
|
||||||
instrument.triggerAttackRelease(hap.duration.valueOf(), time); // noise has no value
|
} else if (instrument instanceof Sampler) {
|
||||||
} else if (instrument instanceof Sampler) {
|
note = getPlayableNoteValue(hap);
|
||||||
note = getPlayableNoteValue(hap);
|
instrument.triggerAttackRelease(note, hap.duration.valueOf(), time, velocity);
|
||||||
instrument.triggerAttackRelease(note, hap.duration.valueOf(), time, velocity);
|
} else if (instrument instanceof Players) {
|
||||||
} else if (instrument instanceof Players) {
|
if (!instrument.has(hap.value)) {
|
||||||
if (!instrument.has(hap.value)) {
|
throw new Error(`name "${hap.value}" not defined for players`);
|
||||||
throw new Error(`name "${hap.value}" not defined for players`);
|
|
||||||
}
|
|
||||||
const player = instrument.player(hap.value);
|
|
||||||
// velocity ?
|
|
||||||
player.start(time);
|
|
||||||
player.stop(time + hap.duration.valueOf());
|
|
||||||
} else {
|
|
||||||
note = getPlayableNoteValue(hap);
|
|
||||||
instrument.triggerAttackRelease(note, hap.duration.valueOf(), time, velocity);
|
|
||||||
}
|
}
|
||||||
};
|
const player = instrument.player(hap.value);
|
||||||
return hap.setContext({ ...hap.context, instrument, onTrigger });
|
// velocity ?
|
||||||
|
player.start(time);
|
||||||
|
player.stop(time + hap.duration.valueOf());
|
||||||
|
} else {
|
||||||
|
note = getPlayableNoteValue(hap);
|
||||||
|
instrument.triggerAttackRelease(note, hap.duration.valueOf(), time, velocity);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,13 +1,87 @@
|
|||||||
|
import { logger } from '@strudel.cycles/core';
|
||||||
|
|
||||||
const bufferCache = {}; // string: Promise<ArrayBuffer>
|
const bufferCache = {}; // string: Promise<ArrayBuffer>
|
||||||
const loadCache = {}; // string: Promise<ArrayBuffer>
|
const loadCache = {}; // string: Promise<ArrayBuffer>
|
||||||
|
|
||||||
export const getCachedBuffer = (url) => bufferCache[url];
|
export const getCachedBuffer = (url) => bufferCache[url];
|
||||||
|
|
||||||
export const loadBuffer = (url, ac) => {
|
function humanFileSize(bytes, si) {
|
||||||
|
var thresh = si ? 1000 : 1024;
|
||||||
|
if (bytes < thresh) return bytes + ' B';
|
||||||
|
var units = si
|
||||||
|
? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
|
||||||
|
: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
|
||||||
|
var u = -1;
|
||||||
|
do {
|
||||||
|
bytes /= thresh;
|
||||||
|
++u;
|
||||||
|
} while (bytes >= thresh);
|
||||||
|
return bytes.toFixed(1) + ' ' + units[u];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getSampleBufferSource = async (s, n, note, speed) => {
|
||||||
|
let transpose = 0;
|
||||||
|
let midi = typeof note === 'string' ? toMidi(note) : note || 36;
|
||||||
|
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) => toMidi(noteA) - midi;
|
||||||
|
// object format will expect keys as notes
|
||||||
|
const closest = Object.keys(bank)
|
||||||
|
.filter((k) => !k.startsWith('_'))
|
||||||
|
.reduce(
|
||||||
|
(closest, key, j) => (!closest || Math.abs(midiDiff(key)) < Math.abs(midiDiff(closest)) ? key : closest),
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
transpose = -midiDiff(closest); // semitones to repitch
|
||||||
|
sampleUrl = bank[closest][n % bank[closest].length];
|
||||||
|
}
|
||||||
|
let buffer = await loadBuffer(sampleUrl, ac, s, n);
|
||||||
|
if (speed < 0) {
|
||||||
|
// should this be cached?
|
||||||
|
buffer = reverseBuffer(buffer);
|
||||||
|
}
|
||||||
|
const bufferSource = ac.createBufferSource();
|
||||||
|
bufferSource.buffer = buffer;
|
||||||
|
const playbackRate = 1.0 * Math.pow(2, transpose / 12);
|
||||||
|
// bufferSource.playbackRate.value = Math.pow(2, transpose / 12);
|
||||||
|
bufferSource.playbackRate.value = playbackRate;
|
||||||
|
return bufferSource;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const loadBuffer = (url, ac, s, n = 0) => {
|
||||||
|
const label = s ? `sound "${s}:${n}"` : 'sample';
|
||||||
if (!loadCache[url]) {
|
if (!loadCache[url]) {
|
||||||
|
logger(`[sampler] load ${label}..`, 'load-sample', { url });
|
||||||
|
const timestamp = Date.now();
|
||||||
loadCache[url] = fetch(url)
|
loadCache[url] = fetch(url)
|
||||||
.then((res) => res.arrayBuffer())
|
.then((res) => res.arrayBuffer())
|
||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
|
const took = Date.now() - timestamp;
|
||||||
|
const size = humanFileSize(res.byteLength);
|
||||||
|
// const downSpeed = humanFileSize(res.byteLength / took);
|
||||||
|
logger(`[sampler] load ${label}... done! loaded ${size} in ${took}ms`, 'loaded-sample', { url });
|
||||||
const decoded = await ac.decodeAudioData(res);
|
const decoded = await ac.decodeAudioData(res);
|
||||||
bufferCache[url] = decoded;
|
bufferCache[url] = decoded;
|
||||||
return decoded;
|
return decoded;
|
||||||
@ -29,66 +103,7 @@ export const getLoadedBuffer = (url) => {
|
|||||||
return bufferCache[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 };
|
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];
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads a collection of samples to use with `s`
|
* Loads a collection of samples to use with `s`
|
||||||
|
|||||||
@ -6,10 +6,10 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
|
|
||||||
// import { Pattern, getFrequency, patternify2 } from '@strudel.cycles/core';
|
// import { Pattern, getFrequency, patternify2 } from '@strudel.cycles/core';
|
||||||
import * as strudel from '@strudel.cycles/core';
|
import * as strudel from '@strudel.cycles/core';
|
||||||
import { fromMidi, isNote, toMidi } from '@strudel.cycles/core';
|
import { fromMidi, logger, toMidi } from '@strudel.cycles/core';
|
||||||
import './feedbackdelay.mjs';
|
import './feedbackdelay.mjs';
|
||||||
import './reverb.mjs';
|
import './reverb.mjs';
|
||||||
import { loadBuffer, reverseBuffer } from './sampler.mjs';
|
import { getSampleBufferSource } from './sampler.mjs';
|
||||||
const { Pattern } = strudel;
|
const { Pattern } = strudel;
|
||||||
import './vowel.mjs';
|
import './vowel.mjs';
|
||||||
import workletsUrl from './worklets.mjs?url';
|
import workletsUrl from './worklets.mjs?url';
|
||||||
@ -98,56 +98,6 @@ const getSoundfontKey = (s) => {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSampleBufferSource = async (s, n, note, speed) => {
|
|
||||||
let transpose = 0;
|
|
||||||
let midi = typeof note === 'string' ? toMidi(note) : note || 36;
|
|
||||||
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) => toMidi(noteA) - midi;
|
|
||||||
// object format will expect keys as notes
|
|
||||||
const closest = Object.keys(bank)
|
|
||||||
.filter((k) => !k.startsWith('_'))
|
|
||||||
.reduce(
|
|
||||||
(closest, key, j) => (!closest || Math.abs(midiDiff(key)) < Math.abs(midiDiff(closest)) ? key : closest),
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
transpose = -midiDiff(closest); // semitones to repitch
|
|
||||||
sampleUrl = bank[closest][n % bank[closest].length];
|
|
||||||
}
|
|
||||||
let buffer = await loadBuffer(sampleUrl, ac);
|
|
||||||
if (speed < 0) {
|
|
||||||
// should this be cached?
|
|
||||||
buffer = reverseBuffer(buffer);
|
|
||||||
}
|
|
||||||
const bufferSource = ac.createBufferSource();
|
|
||||||
bufferSource.buffer = buffer;
|
|
||||||
const playbackRate = 1.0 * Math.pow(2, transpose / 12);
|
|
||||||
// bufferSource.playbackRate.value = Math.pow(2, transpose / 12);
|
|
||||||
bufferSource.playbackRate.value = playbackRate;
|
|
||||||
return bufferSource;
|
|
||||||
};
|
|
||||||
|
|
||||||
const splitSN = (s, n) => {
|
const splitSN = (s, n) => {
|
||||||
if (!s.includes(':')) {
|
if (!s.includes(':')) {
|
||||||
return [s, n];
|
return [s, n];
|
||||||
@ -176,14 +126,25 @@ function getWorklet(ac, processor, params) {
|
|||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof window !== 'undefined') {
|
// this function should be called on first user interaction (to avoid console warning)
|
||||||
try {
|
export function initAudio() {
|
||||||
loadWorklets();
|
if (typeof window !== 'undefined') {
|
||||||
} catch (err) {
|
try {
|
||||||
console.warn('could not load AudioWorklet effects coarse, crush and shape', err);
|
getAudioContext().resume();
|
||||||
|
loadWorklets();
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('could not load AudioWorklet effects coarse, crush and shape', err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function initAudioOnFirstClick() {
|
||||||
|
document.addEventListener('click', function listener() {
|
||||||
|
initAudio();
|
||||||
|
document.removeEventListener('click', listener);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function gainNode(value) {
|
function gainNode(value) {
|
||||||
const node = getAudioContext().createGain();
|
const node = getAudioContext().createGain();
|
||||||
node.gain.value = value;
|
node.gain.value = value;
|
||||||
@ -229,202 +190,194 @@ function effectSend(input, effect, wet) {
|
|||||||
|
|
||||||
// export const webaudioOutput = async (t, hap, ct, cps) => {
|
// export const webaudioOutput = async (t, hap, ct, cps) => {
|
||||||
export const webaudioOutput = async (hap, deadline, hapDuration) => {
|
export const webaudioOutput = async (hap, deadline, hapDuration) => {
|
||||||
try {
|
const ac = getAudioContext();
|
||||||
const ac = getAudioContext();
|
/* if (isNote(hap.value)) {
|
||||||
/* if (isNote(hap.value)) {
|
|
||||||
// supports primitive hap values that look like notes
|
// supports primitive hap values that look like notes
|
||||||
hap.value = { note: hap.value };
|
hap.value = { note: hap.value };
|
||||||
} */
|
} */
|
||||||
if (typeof hap.value !== 'object') {
|
if (typeof hap.value !== 'object') {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`hap.value ${hap.value} is not supported by webaudio output. Hint: append .note() or .s() to the end`,
|
`hap.value ${hap.value} is not supported by webaudio output. Hint: append .note() or .s() to the end`,
|
||||||
);
|
);
|
||||||
}
|
|
||||||
// calculate correct time (tone.js workaround)
|
|
||||||
let t = ac.currentTime + deadline;
|
|
||||||
// destructure value
|
|
||||||
let {
|
|
||||||
freq,
|
|
||||||
s,
|
|
||||||
bank,
|
|
||||||
sf,
|
|
||||||
clip = 0, // if 1, samples will be cut off when the hap ends
|
|
||||||
n = 0,
|
|
||||||
note,
|
|
||||||
gain = 0.8,
|
|
||||||
cutoff,
|
|
||||||
resonance = 1,
|
|
||||||
hcutoff,
|
|
||||||
hresonance = 1,
|
|
||||||
bandf,
|
|
||||||
bandq = 1,
|
|
||||||
coarse,
|
|
||||||
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,
|
|
||||||
roomsize = size,
|
|
||||||
} = hap.value;
|
|
||||||
const { velocity = 1 } = hap.context;
|
|
||||||
gain *= velocity; // legacy fix for velocity
|
|
||||||
// the chain will hold all audio nodes that connect to each other
|
|
||||||
const chain = [];
|
|
||||||
if (bank && s) {
|
|
||||||
s = `${bank}_${s}`;
|
|
||||||
}
|
|
||||||
if (typeof s === 'string') {
|
|
||||||
[s, n] = splitSN(s, n);
|
|
||||||
}
|
|
||||||
if (typeof note === 'string') {
|
|
||||||
[note, n] = splitSN(note, n);
|
|
||||||
}
|
|
||||||
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 {
|
|
||||||
// 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;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (soundfont) {
|
|
||||||
// is soundfont
|
|
||||||
bufferSource = await globalThis.getFontBufferSource(soundfont, note || n, ac);
|
|
||||||
} else {
|
|
||||||
// is sample from loaded samples(..)
|
|
||||||
bufferSource = await getSampleBufferSource(s, n, note, speed);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.warn(err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// asny stuff above took too long?
|
|
||||||
if (ac.currentTime > t) {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
let duration = soundfont || clip ? hapDuration : bufferSource.buffer.duration / bufferSource.playbackRate.value;
|
|
||||||
// "The computation of the offset into the sound is performed using the sound buffer's natural sample rate,
|
|
||||||
// rather than the current playback rate, so even if the sound is playing at twice its normal speed,
|
|
||||||
// the midway point through a 10-second audio buffer is still 5."
|
|
||||||
const offset = begin * duration * bufferSource.playbackRate.value;
|
|
||||||
duration = (end - begin) * duration;
|
|
||||||
if (loop) {
|
|
||||||
bufferSource.loop = true;
|
|
||||||
bufferSource.loopStart = offset;
|
|
||||||
bufferSource.loopEnd = offset + duration;
|
|
||||||
duration = loop * duration;
|
|
||||||
}
|
|
||||||
t += nudge;
|
|
||||||
|
|
||||||
bufferSource.start(t, offset);
|
|
||||||
if (cut !== undefined) {
|
|
||||||
cutGroups[cut]?.stop(t); // fade out?
|
|
||||||
cutGroups[cut] = bufferSource;
|
|
||||||
}
|
|
||||||
chain.push(bufferSource);
|
|
||||||
bufferSource.stop(t + duration + release);
|
|
||||||
const adsr = getADSR(attack, decay, sustain, release, 1, t, t + duration);
|
|
||||||
chain.push(adsr);
|
|
||||||
}
|
|
||||||
|
|
||||||
// gain stage
|
|
||||||
chain.push(gainNode(gain));
|
|
||||||
|
|
||||||
// filters
|
|
||||||
cutoff !== undefined && chain.push(getFilter('lowpass', cutoff, resonance));
|
|
||||||
hcutoff !== undefined && chain.push(getFilter('highpass', hcutoff, hresonance));
|
|
||||||
bandf !== undefined && chain.push(getFilter('bandpass', bandf, bandq));
|
|
||||||
vowel !== undefined && chain.push(ac.createVowelFilter(vowel));
|
|
||||||
|
|
||||||
// effects
|
|
||||||
coarse !== undefined && chain.push(getWorklet(ac, 'coarse-processor', { coarse }));
|
|
||||||
crush !== undefined && chain.push(getWorklet(ac, 'crush-processor', { crush }));
|
|
||||||
shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape }));
|
|
||||||
|
|
||||||
// panning
|
|
||||||
if (pan !== undefined) {
|
|
||||||
const panner = ac.createStereoPanner();
|
|
||||||
panner.pan.value = 2 * pan - 1;
|
|
||||||
chain.push(panner);
|
|
||||||
}
|
|
||||||
|
|
||||||
// last gain
|
|
||||||
const post = gainNode(1);
|
|
||||||
chain.push(post);
|
|
||||||
post.connect(getDestination());
|
|
||||||
|
|
||||||
// delay
|
|
||||||
let delaySend;
|
|
||||||
if (delay > 0 && delaytime > 0 && delayfeedback > 0) {
|
|
||||||
const delyNode = getDelay(orbit, delaytime, delayfeedback, t);
|
|
||||||
delaySend = effectSend(post, delyNode, delay);
|
|
||||||
}
|
|
||||||
// reverb
|
|
||||||
let reverbSend;
|
|
||||||
if (room > 0 && roomsize > 0) {
|
|
||||||
const reverbNode = getReverb(orbit, roomsize);
|
|
||||||
reverbSend = effectSend(post, reverbNode, room);
|
|
||||||
}
|
|
||||||
|
|
||||||
// connect chain elements together
|
|
||||||
chain.slice(1).reduce((last, current) => last.connect(current), chain[0]);
|
|
||||||
|
|
||||||
// disconnect all nodes when source node has ended:
|
|
||||||
chain[0].onended = () => chain.concat([delaySend, reverbSend]).forEach((n) => n?.disconnect());
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('.out error:', e);
|
|
||||||
}
|
}
|
||||||
|
// calculate correct time (tone.js workaround)
|
||||||
|
let t = ac.currentTime + deadline;
|
||||||
|
// destructure value
|
||||||
|
let {
|
||||||
|
freq,
|
||||||
|
s,
|
||||||
|
bank,
|
||||||
|
sf,
|
||||||
|
clip = 0, // if 1, samples will be cut off when the hap ends
|
||||||
|
n = 0,
|
||||||
|
note,
|
||||||
|
gain = 0.8,
|
||||||
|
cutoff,
|
||||||
|
resonance = 1,
|
||||||
|
hcutoff,
|
||||||
|
hresonance = 1,
|
||||||
|
bandf,
|
||||||
|
bandq = 1,
|
||||||
|
coarse,
|
||||||
|
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,
|
||||||
|
roomsize = size,
|
||||||
|
} = hap.value;
|
||||||
|
const { velocity = 1 } = hap.context;
|
||||||
|
gain *= velocity; // legacy fix for velocity
|
||||||
|
// the chain will hold all audio nodes that connect to each other
|
||||||
|
const chain = [];
|
||||||
|
if (bank && s) {
|
||||||
|
s = `${bank}_${s}`;
|
||||||
|
}
|
||||||
|
if (typeof s === 'string') {
|
||||||
|
[s, n] = splitSN(s, n);
|
||||||
|
}
|
||||||
|
if (typeof note === 'string') {
|
||||||
|
[note, n] = splitSN(note, n);
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
// 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);
|
||||||
|
} else {
|
||||||
|
// is sample from loaded samples(..)
|
||||||
|
bufferSource = await getSampleBufferSource(s, n, note, speed);
|
||||||
|
}
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
let duration = soundfont || clip ? hapDuration : bufferSource.buffer.duration / bufferSource.playbackRate.value;
|
||||||
|
// "The computation of the offset into the sound is performed using the sound buffer's natural sample rate,
|
||||||
|
// rather than the current playback rate, so even if the sound is playing at twice its normal speed,
|
||||||
|
// the midway point through a 10-second audio buffer is still 5."
|
||||||
|
const offset = begin * duration * bufferSource.playbackRate.value;
|
||||||
|
duration = (end - begin) * duration;
|
||||||
|
if (loop) {
|
||||||
|
bufferSource.loop = true;
|
||||||
|
bufferSource.loopStart = offset;
|
||||||
|
bufferSource.loopEnd = offset + duration;
|
||||||
|
duration = loop * duration;
|
||||||
|
}
|
||||||
|
t += nudge;
|
||||||
|
|
||||||
|
bufferSource.start(t, offset);
|
||||||
|
if (cut !== undefined) {
|
||||||
|
cutGroups[cut]?.stop(t); // fade out?
|
||||||
|
cutGroups[cut] = bufferSource;
|
||||||
|
}
|
||||||
|
chain.push(bufferSource);
|
||||||
|
bufferSource.stop(t + duration + release);
|
||||||
|
const adsr = getADSR(attack, decay, sustain, release, 1, t, t + duration);
|
||||||
|
chain.push(adsr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// gain stage
|
||||||
|
chain.push(gainNode(gain));
|
||||||
|
|
||||||
|
// filters
|
||||||
|
cutoff !== undefined && chain.push(getFilter('lowpass', cutoff, resonance));
|
||||||
|
hcutoff !== undefined && chain.push(getFilter('highpass', hcutoff, hresonance));
|
||||||
|
bandf !== undefined && chain.push(getFilter('bandpass', bandf, bandq));
|
||||||
|
vowel !== undefined && chain.push(ac.createVowelFilter(vowel));
|
||||||
|
|
||||||
|
// effects
|
||||||
|
coarse !== undefined && chain.push(getWorklet(ac, 'coarse-processor', { coarse }));
|
||||||
|
crush !== undefined && chain.push(getWorklet(ac, 'crush-processor', { crush }));
|
||||||
|
shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape }));
|
||||||
|
|
||||||
|
// panning
|
||||||
|
if (pan !== undefined) {
|
||||||
|
const panner = ac.createStereoPanner();
|
||||||
|
panner.pan.value = 2 * pan - 1;
|
||||||
|
chain.push(panner);
|
||||||
|
}
|
||||||
|
|
||||||
|
// last gain
|
||||||
|
const post = gainNode(1);
|
||||||
|
chain.push(post);
|
||||||
|
post.connect(getDestination());
|
||||||
|
|
||||||
|
// delay
|
||||||
|
let delaySend;
|
||||||
|
if (delay > 0 && delaytime > 0 && delayfeedback > 0) {
|
||||||
|
const delyNode = getDelay(orbit, delaytime, delayfeedback, t);
|
||||||
|
delaySend = effectSend(post, delyNode, delay);
|
||||||
|
}
|
||||||
|
// reverb
|
||||||
|
let reverbSend;
|
||||||
|
if (room > 0 && roomsize > 0) {
|
||||||
|
const reverbNode = getReverb(orbit, roomsize);
|
||||||
|
reverbSend = effectSend(post, reverbNode, room);
|
||||||
|
}
|
||||||
|
|
||||||
|
// connect chain elements together
|
||||||
|
chain.slice(1).reduce((last, current) => last.connect(current), chain[0]);
|
||||||
|
|
||||||
|
// 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);
|
export const webaudioOutputTrigger = (t, hap, ct, cps) => webaudioOutput(hap, t - ct, hap.duration / cps);
|
||||||
|
|||||||
@ -62,37 +62,34 @@ export function loadWebDirt(config) {
|
|||||||
*/
|
*/
|
||||||
Pattern.prototype.webdirt = function () {
|
Pattern.prototype.webdirt = function () {
|
||||||
// create a WebDirt object and initialize Web Audio context
|
// create a WebDirt object and initialize Web Audio context
|
||||||
return this._withHap((hap) => {
|
return this.onTrigger(async (time, e, currentTime) => {
|
||||||
const onTrigger = async (time, e, currentTime) => {
|
if (!webDirt) {
|
||||||
if (!webDirt) {
|
throw new Error('WebDirt not initialized!');
|
||||||
throw new Error('WebDirt not initialized!');
|
}
|
||||||
}
|
const deadline = time - currentTime;
|
||||||
const deadline = time - currentTime;
|
const { s, n = 0, ...rest } = e.value || {};
|
||||||
const { s, n = 0, ...rest } = e.value || {};
|
if (!s) {
|
||||||
if (!s) {
|
console.warn('Pattern.webdirt: no "s" was set!');
|
||||||
console.warn('Pattern.webdirt: no "s" was set!');
|
}
|
||||||
}
|
const samples = getLoadedSamples();
|
||||||
const samples = getLoadedSamples();
|
if (!samples?.[s]) {
|
||||||
if (!samples?.[s]) {
|
// try default samples
|
||||||
// try default samples
|
webDirt.playSample({ s, n, ...rest }, deadline);
|
||||||
webDirt.playSample({ s, n, ...rest }, deadline);
|
return;
|
||||||
return;
|
}
|
||||||
}
|
if (!samples?.[s]) {
|
||||||
if (!samples?.[s]) {
|
console.warn(`Pattern.webdirt: sample "${s}" not found in loaded samples`, samples);
|
||||||
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 {
|
} else {
|
||||||
const bank = samples[s];
|
const msg = { buffer: { buffer }, ...rest };
|
||||||
const sampleUrl = bank[n % bank.length];
|
webDirt.playSample(msg, deadline);
|
||||||
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 });
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
15
repl/package-lock.json
generated
15
repl/package-lock.json
generated
@ -8,6 +8,7 @@
|
|||||||
"name": "@strudel.cycles/repl",
|
"name": "@strudel.cycles/repl",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@heroicons/react": "^2.0.13",
|
||||||
"@supabase/supabase-js": "^1.35.3",
|
"@supabase/supabase-js": "^1.35.3",
|
||||||
"nanoid": "^4.0.0",
|
"nanoid": "^4.0.0",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
@ -449,6 +450,14 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@heroicons/react": {
|
||||||
|
"version": "2.0.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.0.13.tgz",
|
||||||
|
"integrity": "sha512-iSN5XwmagrnirWlYEWNPdCDj9aRYVD/lnK3JlsC9/+fqGF80k8C7rl+1HCvBX0dBoagKqOFBs6fMhJJ1hOg1EQ==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">= 16"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@jridgewell/gen-mapping": {
|
"node_modules/@jridgewell/gen-mapping": {
|
||||||
"version": "0.1.1",
|
"version": "0.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz",
|
||||||
@ -2972,6 +2981,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"@heroicons/react": {
|
||||||
|
"version": "2.0.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.0.13.tgz",
|
||||||
|
"integrity": "sha512-iSN5XwmagrnirWlYEWNPdCDj9aRYVD/lnK3JlsC9/+fqGF80k8C7rl+1HCvBX0dBoagKqOFBs6fMhJJ1hOg1EQ==",
|
||||||
|
"requires": {}
|
||||||
|
},
|
||||||
"@jridgewell/gen-mapping": {
|
"@jridgewell/gen-mapping": {
|
||||||
"version": "0.1.1",
|
"version": "0.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz",
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"predev": "cd ${PWD}/../tutorial/ && npm run render",
|
||||||
"dev": "vite --host",
|
"dev": "vite --host",
|
||||||
"start": "vite",
|
"start": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
@ -16,6 +17,7 @@
|
|||||||
"dbdump": "node src/test/dbdump.js > src/test/dbdump.json"
|
"dbdump": "node src/test/dbdump.js > src/test/dbdump.json"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@heroicons/react": "^2.0.13",
|
||||||
"@supabase/supabase-js": "^1.35.3",
|
"@supabase/supabase-js": "^1.35.3",
|
||||||
"nanoid": "^4.0.0",
|
"nanoid": "^4.0.0",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
|
|||||||
@ -18,3 +18,18 @@ body {
|
|||||||
background: black;
|
background: black;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cm-gutters {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-content {
|
||||||
|
padding-top: 12px !important;
|
||||||
|
padding-left: 8px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
/* font-family: 'monospace'; */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
479
repl/src/App.jsx
479
repl/src/App.jsx
@ -4,19 +4,26 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
|
|||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { evaluate } from '@strudel.cycles/eval';
|
import { cleanupDraw, cleanupUi, controls, evalScope, logger } from '@strudel.cycles/core';
|
||||||
import { CodeMirror, cx, flash, useHighlighting, useRepl, useWebMidi } from '@strudel.cycles/react';
|
import { CodeMirror, cx, flash, useHighlighting, useStrudel } from '@strudel.cycles/react';
|
||||||
import { cleanupDraw, cleanupUi, Tone } from '@strudel.cycles/tone';
|
import {
|
||||||
import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
getAudioContext,
|
||||||
import './App.css';
|
getLoadedSamples,
|
||||||
import logo from './logo.svg';
|
initAudioOnFirstClick,
|
||||||
import * as tunes from './tunes.mjs';
|
resetLoadedSamples,
|
||||||
import { prebake } from './prebake.mjs';
|
webaudioOutput,
|
||||||
import * as WebDirt from 'WebDirt';
|
} from '@strudel.cycles/webaudio';
|
||||||
import { resetLoadedSamples, getAudioContext } from '@strudel.cycles/webaudio';
|
|
||||||
import { controls, evalScope } from '@strudel.cycles/core';
|
|
||||||
import { createClient } from '@supabase/supabase-js';
|
import { createClient } from '@supabase/supabase-js';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
|
import React, { createContext, useCallback, useEffect, useState } from 'react';
|
||||||
|
import * as WebDirt from 'WebDirt';
|
||||||
|
import './App.css';
|
||||||
|
import { Footer } from './Footer';
|
||||||
|
import { Header } from './Header';
|
||||||
|
import { prebake } from './prebake.mjs';
|
||||||
|
import * as tunes from './tunes.mjs';
|
||||||
|
|
||||||
|
initAudioOnFirstClick();
|
||||||
|
|
||||||
// Create a single supabase client for interacting with your database
|
// Create a single supabase client for interacting with your database
|
||||||
const supabase = createClient(
|
const supabase = createClient(
|
||||||
@ -24,12 +31,9 @@ const supabase = createClient(
|
|||||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InBpZHhkc3hwaGxoempuem1pZnRoIiwicm9sZSI6ImFub24iLCJpYXQiOjE2NTYyMzA1NTYsImV4cCI6MTk3MTgwNjU1Nn0.bqlw7802fsWRnqU5BLYtmXk_k-D1VFmbkHMywWc15NM',
|
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InBpZHhkc3hwaGxoempuem1pZnRoIiwicm9sZSI6ImFub24iLCJpYXQiOjE2NTYyMzA1NTYsImV4cCI6MTk3MTgwNjU1Nn0.bqlw7802fsWRnqU5BLYtmXk_k-D1VFmbkHMywWc15NM',
|
||||||
);
|
);
|
||||||
|
|
||||||
evalScope(
|
const modules = [
|
||||||
Tone,
|
|
||||||
controls, // sadly, this cannot be exported from core direclty
|
|
||||||
{ WebDirt },
|
|
||||||
import('@strudel.cycles/core'),
|
import('@strudel.cycles/core'),
|
||||||
import('@strudel.cycles/tone'),
|
// import('@strudel.cycles/tone'),
|
||||||
import('@strudel.cycles/tonal'),
|
import('@strudel.cycles/tonal'),
|
||||||
import('@strudel.cycles/mini'),
|
import('@strudel.cycles/mini'),
|
||||||
import('@strudel.cycles/midi'),
|
import('@strudel.cycles/midi'),
|
||||||
@ -38,9 +42,24 @@ evalScope(
|
|||||||
import('@strudel.cycles/osc'),
|
import('@strudel.cycles/osc'),
|
||||||
import('@strudel.cycles/serial'),
|
import('@strudel.cycles/serial'),
|
||||||
import('@strudel.cycles/soundfonts'),
|
import('@strudel.cycles/soundfonts'),
|
||||||
|
];
|
||||||
|
|
||||||
|
evalScope(
|
||||||
|
// Tone,
|
||||||
|
controls, // sadly, this cannot be exported from core direclty
|
||||||
|
{ WebDirt },
|
||||||
|
...modules,
|
||||||
);
|
);
|
||||||
|
|
||||||
prebake();
|
export let loadedSamples = [];
|
||||||
|
const presets = prebake();
|
||||||
|
|
||||||
|
Promise.all([...modules, presets]).then((data) => {
|
||||||
|
// console.log('modules and sample registry loade', data);
|
||||||
|
loadedSamples = Object.entries(getLoadedSamples() || {});
|
||||||
|
});
|
||||||
|
|
||||||
|
const getTime = () => getAudioContext().currentTime;
|
||||||
|
|
||||||
async function initCode() {
|
async function initCode() {
|
||||||
// load code from url hash (either short hash from database or decode long hash)
|
// load code from url hash (either short hash from database or decode long hash)
|
||||||
@ -50,7 +69,6 @@ async function initCode() {
|
|||||||
const codeParam = window.location.href.split('#')[1];
|
const codeParam = window.location.href.split('#')[1];
|
||||||
// looking like https://strudel.tidalcycles.org/?J01s5i1J0200 (fixed hash length)
|
// looking like https://strudel.tidalcycles.org/?J01s5i1J0200 (fixed hash length)
|
||||||
if (codeParam) {
|
if (codeParam) {
|
||||||
console.log('decode hash from url');
|
|
||||||
// looking like https://strudel.tidalcycles.org/#ImMzIGUzIg%3D%3D (hash length depends on code length)
|
// looking like https://strudel.tidalcycles.org/#ImMzIGUzIg%3D%3D (hash length depends on code length)
|
||||||
return atob(decodeURIComponent(codeParam || ''));
|
return atob(decodeURIComponent(codeParam || ''));
|
||||||
} else if (hash) {
|
} else if (hash) {
|
||||||
@ -74,269 +92,202 @@ async function initCode() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getRandomTune() {
|
function getRandomTune() {
|
||||||
const allTunes = Object.values(tunes);
|
const allTunes = Object.entries(tunes);
|
||||||
const randomItem = (arr) => arr[Math.floor(Math.random() * arr.length)];
|
const randomItem = (arr) => arr[Math.floor(Math.random() * arr.length)];
|
||||||
return randomItem(allTunes);
|
const [name, code] = randomItem(allTunes);
|
||||||
|
return { name, code };
|
||||||
}
|
}
|
||||||
|
|
||||||
const randomTune = getRandomTune();
|
const { code: randomTune, name } = getRandomTune();
|
||||||
const isEmbedded = window.location !== window.parent.location;
|
|
||||||
|
export const AppContext = createContext();
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
// const [editor, setEditor] = useState();
|
const [view, setView] = useState(); // codemirror view
|
||||||
const [view, setView] = useState();
|
|
||||||
const [lastShared, setLastShared] = useState();
|
const [lastShared, setLastShared] = useState();
|
||||||
const {
|
const [activeFooter, setActiveFooter] = useState('');
|
||||||
setCode,
|
const [isZen, setIsZen] = useState(false);
|
||||||
setPattern,
|
const [pending, setPending] = useState(false);
|
||||||
error,
|
|
||||||
code,
|
const { code, setCode, scheduler, evaluate, activateCode, isDirty, activeCode, pattern, started, stop, error } =
|
||||||
cycle,
|
useStrudel({
|
||||||
dirty,
|
initialCode: '// LOADING',
|
||||||
log,
|
defaultOutput: webaudioOutput,
|
||||||
togglePlay,
|
getTime,
|
||||||
activeCode,
|
autolink: true,
|
||||||
setActiveCode,
|
beforeEval: () => {
|
||||||
activateCode,
|
cleanupUi();
|
||||||
pattern,
|
cleanupDraw();
|
||||||
pushLog,
|
setPending(true);
|
||||||
pending,
|
},
|
||||||
hideHeader,
|
afterEval: () => {
|
||||||
hideConsole,
|
setPending(false);
|
||||||
} = useRepl({
|
},
|
||||||
tune: '// LOADING...',
|
onToggle: (play) => !play && cleanupDraw(false),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// init code
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
initCode().then((decoded) => setCode(decoded || randomTune));
|
initCode().then((decoded) => {
|
||||||
}, []);
|
if (!decoded) {
|
||||||
const logBox = useRef();
|
setActiveFooter('intro'); // TODO: get rid
|
||||||
// scroll log box to bottom when log changes
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
if (logBox.current) {
|
|
||||||
logBox.current.scrollTop = logBox.current?.scrollHeight;
|
|
||||||
}
|
|
||||||
}, [log]);
|
|
||||||
|
|
||||||
// set active pattern on ctrl+enter
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
// TODO: make sure this is only fired when editor has focus
|
|
||||||
const handleKeyPress = async (e) => {
|
|
||||||
if (e.ctrlKey || e.altKey) {
|
|
||||||
if (e.code === 'Enter') {
|
|
||||||
e.preventDefault();
|
|
||||||
flash(view);
|
|
||||||
await activateCode();
|
|
||||||
} else if (e.code === 'Period') {
|
|
||||||
cycle.stop();
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
logger(
|
||||||
window.addEventListener('keydown', handleKeyPress, true);
|
`Welcome to Strudel! ${
|
||||||
return () => window.removeEventListener('keydown', handleKeyPress, true);
|
decoded ? `I have loaded the code from the URL.` : `A random code snippet named "${name}" has been loaded!`
|
||||||
}, [pattern, code, activateCode, cycle, view]);
|
} Press play or hit ctrl+enter to run it!`,
|
||||||
|
'highlight',
|
||||||
|
);
|
||||||
|
setCode(decoded || randomTune);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// keyboard shortcuts
|
||||||
|
useKeydown(
|
||||||
|
useCallback(
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[activateCode, stop, view],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// highlighting
|
||||||
useHighlighting({
|
useHighlighting({
|
||||||
view,
|
view,
|
||||||
pattern,
|
pattern,
|
||||||
active: cycle.started && !activeCode?.includes('strudel disable-highlighting'),
|
active: started && !activeCode?.includes('strudel disable-highlighting'),
|
||||||
getTime: () => Tone.getTransport().seconds,
|
getTime: () => scheduler.getPhase(),
|
||||||
});
|
});
|
||||||
|
|
||||||
useWebMidi({
|
//
|
||||||
ready: useCallback(
|
// UI Actions
|
||||||
({ outputs }) => {
|
//
|
||||||
pushLog(`WebMidi ready! Just add .midi(${outputs.map((o) => `'${o.name}'`).join(' | ')}) to the pattern. `);
|
|
||||||
},
|
const handleChangeCode = useCallback(
|
||||||
[pushLog],
|
(c) => {
|
||||||
),
|
setCode(c);
|
||||||
connected: useCallback(
|
started && logger('[edit] code changed. hit ctrl+enter to update');
|
||||||
({ outputs }) => {
|
},
|
||||||
pushLog(`Midi device connected! Available: ${outputs.map((o) => `'${o.name}'`).join(', ')}`);
|
[started],
|
||||||
},
|
);
|
||||||
[pushLog],
|
const handleSelectionChange = useCallback((selection) => {
|
||||||
),
|
// TODO: scroll to selected function in reference
|
||||||
disconnected: useCallback(
|
// console.log('selectino change', selection.ranges[0].from);
|
||||||
({ outputs }) => {
|
}, []);
|
||||||
pushLog(`Midi device disconnected! Available: ${outputs.map((o) => `'${o.name}'`).join(', ')}`);
|
const handleTogglePlay = async () => {
|
||||||
},
|
await getAudioContext().resume(); // fixes no sound in ios webkit
|
||||||
[pushLog],
|
if (!started) {
|
||||||
),
|
logger('[repl] started. tip: you can also start by pressing ctrl+enter', 'highlight');
|
||||||
});
|
activateCode();
|
||||||
|
} else {
|
||||||
|
logger('[repl] stopped. tip: you can also stop by pressing ctrl+dot', 'highlight');
|
||||||
|
stop();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const handleUpdate = () => {
|
||||||
|
isDirty && activateCode();
|
||||||
|
logger('[repl] code updated! tip: you can also update the code by pressing ctrl+enter', 'highlight');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleShuffle = async () => {
|
||||||
|
const { code, name } = getRandomTune();
|
||||||
|
logger(`[repl] ✨ loading random tune "${name}"`);
|
||||||
|
resetLoadedSamples();
|
||||||
|
await prebake(); // declare default samples
|
||||||
|
await evaluate(code, false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleShare = async () => {
|
||||||
|
const codeToShare = activeCode || code;
|
||||||
|
if (lastShared === codeToShare) {
|
||||||
|
logger(`Link already generated!`, 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// generate uuid in the browser
|
||||||
|
const hash = nanoid(12);
|
||||||
|
const { data, error } = await supabase.from('code').insert([{ code: codeToShare, hash }]);
|
||||||
|
if (!error) {
|
||||||
|
setLastShared(activeCode || code);
|
||||||
|
const shareUrl = window.location.origin + '?' + hash;
|
||||||
|
// copy shareUrl to clipboard
|
||||||
|
navigator.clipboard.writeText(shareUrl);
|
||||||
|
const message = `Link copied to clipboard: ${shareUrl}`;
|
||||||
|
alert(message);
|
||||||
|
// alert(message);
|
||||||
|
logger(message, 'highlight');
|
||||||
|
} else {
|
||||||
|
console.log('error', error);
|
||||||
|
const message = `Error: ${error.message}`;
|
||||||
|
// alert(message);
|
||||||
|
logger(message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex flex-col">
|
// bg-gradient-to-t from-blue-900 to-slate-900
|
||||||
{!hideHeader && (
|
// bg-gradient-to-t from-green-900 to-slate-900
|
||||||
<header
|
<AppContext.Provider
|
||||||
id="header"
|
value={{
|
||||||
className={cx(
|
started,
|
||||||
'flex-none w-full px-2 flex border-b border-gray-200 justify-between z-[10] bg-gray-100',
|
pending,
|
||||||
isEmbedded ? 'h-8' : 'h-14',
|
isDirty,
|
||||||
)}
|
lastShared,
|
||||||
>
|
activeCode,
|
||||||
<div className="flex items-center space-x-2">
|
activeFooter,
|
||||||
<img src={logo} className={cx('Tidal-logo', isEmbedded ? 'w-6 h-6' : 'w-10 h-10')} alt="logo" />
|
setActiveFooter,
|
||||||
<h1 className={isEmbedded ? 'text-l' : 'text-xl'}>Strudel {isEmbedded ? 'Mini ' : ''}REPL</h1>
|
handleChangeCode,
|
||||||
</div>
|
handleTogglePlay,
|
||||||
<div className="flex">
|
handleUpdate,
|
||||||
<button
|
handleShuffle,
|
||||||
onClick={async () => {
|
handleShare,
|
||||||
await getAudioContext().resume(); // fixes no sound in ios webkit
|
isZen,
|
||||||
togglePlay();
|
setIsZen,
|
||||||
}}
|
}}
|
||||||
className={cx('hover:bg-gray-300', !isEmbedded ? 'p-2' : 'px-2')}
|
>
|
||||||
>
|
<div
|
||||||
{!pending ? (
|
className={cx(
|
||||||
<span className={cx('flex items-center', isEmbedded ? 'w-16' : 'w-16')}>
|
'h-screen flex flex-col',
|
||||||
{cycle.started ? (
|
// 'bg-gradient-to-t from-green-900 to-slate-900', //
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
|
||||||
<path
|
|
||||||
fillRule="evenodd"
|
|
||||||
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v4a1 1 0 102 0V8a1 1 0 00-1-1z"
|
|
||||||
clipRule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
) : (
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
|
||||||
<path
|
|
||||||
fillRule="evenodd"
|
|
||||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z"
|
|
||||||
clipRule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
)}
|
|
||||||
{cycle.started ? 'pause' : 'play'}
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<>loading...</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
dirty && activateCode();
|
|
||||||
pushLog('Code updated! Tip: You can also update the code by pressing ctrl+enter.');
|
|
||||||
}}
|
|
||||||
className={cx(
|
|
||||||
'hover:bg-gray-300',
|
|
||||||
!isEmbedded ? 'p-2' : 'px-2',
|
|
||||||
!dirty || !activeCode ? 'opacity-50' : '',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
🔄 update
|
|
||||||
</button>
|
|
||||||
{!isEmbedded && (
|
|
||||||
<button
|
|
||||||
className="hover:bg-gray-300 p-2"
|
|
||||||
onClick={async () => {
|
|
||||||
const _code = getRandomTune();
|
|
||||||
// console.log('tune', _code); // uncomment this to debug when random code fails
|
|
||||||
setCode(_code);
|
|
||||||
cleanupDraw();
|
|
||||||
cleanupUi();
|
|
||||||
resetLoadedSamples();
|
|
||||||
await prebake(); // declare default samples
|
|
||||||
const parsed = await evaluate(_code);
|
|
||||||
setPattern(parsed.pattern);
|
|
||||||
setActiveCode(_code);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
🎲 random
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{!isEmbedded && (
|
|
||||||
<button className={cx('hover:bg-gray-300', !isEmbedded ? 'p-2' : 'px-2')}>
|
|
||||||
<a href="./tutorial">📚 tutorial</a>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{!isEmbedded && (
|
|
||||||
<button
|
|
||||||
className={cx('cursor-pointer hover:bg-gray-300', !isEmbedded ? 'p-2' : 'px-2')}
|
|
||||||
onClick={async () => {
|
|
||||||
const codeToShare = activeCode || code;
|
|
||||||
if (lastShared === codeToShare) {
|
|
||||||
// alert('Link already generated!');
|
|
||||||
pushLog(`Link already generated!`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// generate uuid in the browser
|
|
||||||
const hash = nanoid(12);
|
|
||||||
const { data, error } = await supabase.from('code').insert([{ code: codeToShare, hash }]);
|
|
||||||
if (!error) {
|
|
||||||
setLastShared(activeCode || code);
|
|
||||||
const shareUrl = window.location.origin + '?' + hash;
|
|
||||||
// copy shareUrl to clipboard
|
|
||||||
navigator.clipboard.writeText(shareUrl);
|
|
||||||
const message = `Link copied to clipboard: ${shareUrl}`;
|
|
||||||
// alert(message);
|
|
||||||
pushLog(message);
|
|
||||||
} else {
|
|
||||||
console.log('error', error);
|
|
||||||
const message = `Error: ${error.message}`;
|
|
||||||
// alert(message);
|
|
||||||
pushLog(message);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
📣 share{lastShared && lastShared === (activeCode || code) ? 'd!' : ''}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{isEmbedded && (
|
|
||||||
<button className={cx('hover:bg-gray-300 px-2')}>
|
|
||||||
<a href={window.location.href} target="_blank" rel="noopener noreferrer" title="Open in REPL">
|
|
||||||
🚀 open
|
|
||||||
</a>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{isEmbedded && (
|
|
||||||
<button className={cx('hover:bg-gray-300 px-2')}>
|
|
||||||
<a
|
|
||||||
onClick={() => {
|
|
||||||
window.location.href = initialUrl;
|
|
||||||
window.location.reload();
|
|
||||||
}}
|
|
||||||
title="Reset"
|
|
||||||
>
|
|
||||||
💔 reset
|
|
||||||
</a>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
)}
|
|
||||||
<section className="grow flex flex-col text-gray-100">
|
|
||||||
<div className="grow relative flex overflow-auto pb-8 cursor-text" id="code">
|
|
||||||
{/* onCursor={markParens} */}
|
|
||||||
<CodeMirror value={code} onChange={setCode} onViewChanged={setView} />
|
|
||||||
<span className="z-[20] bg-black rounded-t-md py-1 px-2 fixed bottom-0 right-1 text-xs whitespace-pre text-right pointer-events-none">
|
|
||||||
{!cycle.started ? `press ctrl+enter to play\n` : dirty ? `ctrl+enter to update\n` : 'no changes\n'}
|
|
||||||
</span>
|
|
||||||
{error && (
|
|
||||||
<div
|
|
||||||
className={cx(
|
|
||||||
'rounded-md fixed pointer-events-none left-2 bottom-1 text-xs bg-black px-2 z-[20]',
|
|
||||||
'text-red-500',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{error?.message || 'unknown error'}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{!isEmbedded && !hideConsole && (
|
|
||||||
<textarea
|
|
||||||
className="z-[10] h-16 border-0 text-xs bg-[transparent] border-t border-slate-600 resize-none"
|
|
||||||
value={log}
|
|
||||||
readOnly
|
|
||||||
ref={logBox}
|
|
||||||
style={{ fontFamily: 'monospace' }}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</section>
|
>
|
||||||
{/* !isEmbedded && (
|
<Header />
|
||||||
<button className="fixed right-4 bottom-2 z-[11]" onClick={() => playStatic(code)}>
|
<section className="grow flex text-gray-100 relative overflow-auto cursor-text pb-0" id="code">
|
||||||
static
|
<CodeMirror
|
||||||
</button>
|
value={code}
|
||||||
) */}
|
onChange={handleChangeCode}
|
||||||
</div>
|
onViewChanged={setView}
|
||||||
|
onSelectionChange={handleSelectionChange}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
{error && (
|
||||||
|
<div className="text-red-500 p-4 bg-lineblack animate-pulse">{error.message || 'Unknown Error :-/'}</div>
|
||||||
|
)}
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
</AppContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|
||||||
|
export function useEvent(name, onTrigger, useCapture = false) {
|
||||||
|
useEffect(() => {
|
||||||
|
document.addEventListener(name, onTrigger, useCapture);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener(name, onTrigger, useCapture);
|
||||||
|
};
|
||||||
|
}, [onTrigger]);
|
||||||
|
}
|
||||||
|
function useKeydown(onTrigger) {
|
||||||
|
useEvent('keydown', onTrigger, true);
|
||||||
|
}
|
||||||
|
|||||||
196
repl/src/Footer.jsx
Normal file
196
repl/src/Footer.jsx
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
import XMarkIcon from '@heroicons/react/20/solid/XMarkIcon';
|
||||||
|
import { logger } from '@strudel.cycles/core';
|
||||||
|
import { cx } from '@strudel.cycles/react';
|
||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
import React, { useContext, useCallback, useLayoutEffect, useRef, useState } from 'react';
|
||||||
|
import { useEvent, loadedSamples, AppContext } from './App';
|
||||||
|
import { Reference } from './Reference';
|
||||||
|
|
||||||
|
export function Footer() {
|
||||||
|
// const [activeFooter, setActiveFooter] = useState('console');
|
||||||
|
const { activeFooter, setActiveFooter, isZen } = useContext(AppContext);
|
||||||
|
const footerContent = useRef();
|
||||||
|
const [log, setLog] = useState([]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (footerContent.current && activeFooter === 'console') {
|
||||||
|
// scroll log box to bottom when log changes
|
||||||
|
footerContent.current.scrollTop = footerContent.current?.scrollHeight;
|
||||||
|
}
|
||||||
|
}, [log, activeFooter]);
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!footerContent.current) {
|
||||||
|
} else if (activeFooter === 'console') {
|
||||||
|
footerContent.current.scrollTop = footerContent.current?.scrollHeight;
|
||||||
|
} else {
|
||||||
|
footerContent.current.scrollTop = 0;
|
||||||
|
}
|
||||||
|
}, [activeFooter]);
|
||||||
|
|
||||||
|
useLogger(
|
||||||
|
useCallback((e) => {
|
||||||
|
const { message, type, data } = e.detail;
|
||||||
|
setLog((l) => {
|
||||||
|
const lastLog = l.length ? l[l.length - 1] : undefined;
|
||||||
|
const id = nanoid(12);
|
||||||
|
// if (type === 'loaded-sample' && lastLog.type === 'load-sample' && lastLog.url === data.url) {
|
||||||
|
if (type === 'loaded-sample') {
|
||||||
|
// const loadIndex = l.length - 1;
|
||||||
|
const loadIndex = l.findIndex(({ data: { url }, type }) => type === 'load-sample' && url === data.url);
|
||||||
|
l[loadIndex] = { message, type, id, data };
|
||||||
|
} else if (lastLog && lastLog.message === message) {
|
||||||
|
l = l.slice(0, -1).concat([{ message, type, count: (lastLog.count ?? 1) + 1, id, data }]);
|
||||||
|
} else {
|
||||||
|
l = l.concat([{ message, type, id, data }]);
|
||||||
|
}
|
||||||
|
return l.slice(-20);
|
||||||
|
});
|
||||||
|
}, []),
|
||||||
|
);
|
||||||
|
|
||||||
|
const FooterTab = ({ children, name, label }) => (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
onClick={() => setActiveFooter(name)}
|
||||||
|
className={cx(
|
||||||
|
'h-8 px-2 text-white cursor-pointer hover:text-tertiary flex items-center space-x-1 border-b',
|
||||||
|
activeFooter === name ? 'border-white hover:border-tertiary' : 'border-transparent',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{label || name}
|
||||||
|
</div>
|
||||||
|
{activeFooter === name && <>{children}</>}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
if (isZen) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<footer className="bg-footer z-[20]">
|
||||||
|
<div className="flex justify-between px-2">
|
||||||
|
<div className={cx('flex select-none', activeFooter && 'pb-2')}>
|
||||||
|
<FooterTab name="intro" label="welcome" />
|
||||||
|
<FooterTab name="samples" />
|
||||||
|
<FooterTab name="console" />
|
||||||
|
<FooterTab name="reference" />
|
||||||
|
</div>
|
||||||
|
{activeFooter !== '' && (
|
||||||
|
<button onClick={() => setActiveFooter('')} className="text-white">
|
||||||
|
<XMarkIcon className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{activeFooter !== '' && (
|
||||||
|
<div
|
||||||
|
className="text-white font-mono text-sm h-[360px] flex-none overflow-auto max-w-full relative"
|
||||||
|
ref={footerContent}
|
||||||
|
>
|
||||||
|
{activeFooter === 'intro' && (
|
||||||
|
<div className="prose prose-invert max-w-[600px] pt-2 font-sans pb-8 px-4">
|
||||||
|
<h3>
|
||||||
|
<span className={cx('animate-spin inline-block select-none')}>🌀</span> welcome
|
||||||
|
</h3>
|
||||||
|
<p>
|
||||||
|
You have found <span className="underline">strudel</span>, a new live coding platform to write dynamic
|
||||||
|
music pieces in the browser! It is free and open-source and made for beginners and experts alike. To get
|
||||||
|
started:
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<span className="underline">1. hit play</span> - <span className="underline">2. change something</span>{' '}
|
||||||
|
- <span className="underline">3. hit update</span>
|
||||||
|
<br />
|
||||||
|
If you don't like what you hear, try <span className="underline">shuffle</span>!
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
To learn more about what this all means, check out the{' '}
|
||||||
|
<a href="https://strudel.tidalcycles.org/tutorial" target="_blank">
|
||||||
|
interactive tutorial
|
||||||
|
</a>
|
||||||
|
. Also feel free to join the{' '}
|
||||||
|
<a href="https://discord.com/invite/HGEdXmRkzT" target="_blank">
|
||||||
|
tidalcycles discord channel
|
||||||
|
</a>{' '}
|
||||||
|
to ask any questions, give feedback or just say hello.
|
||||||
|
</p>
|
||||||
|
<h3>about</h3>
|
||||||
|
<p>
|
||||||
|
strudel is a JavaScript version of{' '}
|
||||||
|
<a href="tidalcycles.org/" target="_blank">
|
||||||
|
tidalcycles
|
||||||
|
</a>
|
||||||
|
, which is a popular live coding language for music, written in Haskell. You can find the source code at{' '}
|
||||||
|
<a href="https://github.com/tidalcycles/strudel" target="_blank">
|
||||||
|
github
|
||||||
|
</a>
|
||||||
|
. Please consider to{' '}
|
||||||
|
<a href="https://opencollective.com/tidalcycles" target="_blank">
|
||||||
|
support this project
|
||||||
|
</a>{' '}
|
||||||
|
to ensure ongoing development 💖
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{activeFooter === 'console' && (
|
||||||
|
<div className="break-all px-4">
|
||||||
|
{log.map((l, i) => {
|
||||||
|
const message = linkify(l.message);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={l.id}
|
||||||
|
className={cx(l.type === 'error' && 'text-red-500', l.type === 'highlight' && 'text-highlight')}
|
||||||
|
>
|
||||||
|
<span dangerouslySetInnerHTML={{ __html: message }} />
|
||||||
|
{l.count ? ` (${l.count})` : ''}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{activeFooter === 'samples' && (
|
||||||
|
<div className="break-normal w-full px-4">
|
||||||
|
<span className="text-white">{loadedSamples.length} banks loaded:</span>
|
||||||
|
{loadedSamples.map(([name, samples]) => (
|
||||||
|
<span key={name} className="cursor-pointer hover:text-tertiary" onClick={() => {}}>
|
||||||
|
{' '}
|
||||||
|
{name}(
|
||||||
|
{Array.isArray(samples)
|
||||||
|
? samples.length
|
||||||
|
: typeof samples === 'object'
|
||||||
|
? Object.values(samples).length
|
||||||
|
: 1}
|
||||||
|
){' '}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{activeFooter === 'reference' && <Reference />}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function useLogger(onTrigger) {
|
||||||
|
useEvent(logger.key, onTrigger);
|
||||||
|
}
|
||||||
|
|
||||||
|
function linkify(inputText) {
|
||||||
|
var replacedText, replacePattern1, replacePattern2, replacePattern3;
|
||||||
|
|
||||||
|
//URLs starting with http://, https://, or ftp://
|
||||||
|
replacePattern1 = /(\b(https?|ftp):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gim;
|
||||||
|
replacedText = inputText.replace(replacePattern1, '<a class="underline" href="$1" target="_blank">$1</a>');
|
||||||
|
|
||||||
|
//URLs starting with "www." (without // before it, or it'd re-link the ones done above).
|
||||||
|
replacePattern2 = /(^|[^\/])(www\.[\S]+(\b|$))/gim;
|
||||||
|
replacedText = replacedText.replace(
|
||||||
|
replacePattern2,
|
||||||
|
'$1<a class="underline" href="http://$2" target="_blank">$2</a>',
|
||||||
|
);
|
||||||
|
|
||||||
|
//Change email addresses to mailto:: links.
|
||||||
|
replacePattern3 = /(([a-zA-Z0-9\-\_\.])+@[a-zA-Z\_]+?(\.[a-zA-Z]{2,6})+)/gim;
|
||||||
|
replacedText = replacedText.replace(replacePattern3, '<a class="underline" href="mailto:$1">$1</a>');
|
||||||
|
|
||||||
|
return replacedText;
|
||||||
|
}
|
||||||
140
repl/src/Header.jsx
Normal file
140
repl/src/Header.jsx
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
import AcademicCapIcon from '@heroicons/react/20/solid/AcademicCapIcon';
|
||||||
|
import CommandLineIcon from '@heroicons/react/20/solid/CommandLineIcon';
|
||||||
|
import LinkIcon from '@heroicons/react/20/solid/LinkIcon';
|
||||||
|
import PlayCircleIcon from '@heroicons/react/20/solid/PlayCircleIcon';
|
||||||
|
import SparklesIcon from '@heroicons/react/20/solid/SparklesIcon';
|
||||||
|
import StopCircleIcon from '@heroicons/react/20/solid/StopCircleIcon';
|
||||||
|
import { cx } from '@strudel.cycles/react';
|
||||||
|
import React, { useContext } from 'react';
|
||||||
|
import { AppContext } from './App';
|
||||||
|
import './App.css';
|
||||||
|
|
||||||
|
const isEmbedded = window.location !== window.parent.location;
|
||||||
|
|
||||||
|
export function Header() {
|
||||||
|
const {
|
||||||
|
started,
|
||||||
|
pending,
|
||||||
|
isDirty,
|
||||||
|
lastShared,
|
||||||
|
activeCode,
|
||||||
|
handleTogglePlay,
|
||||||
|
handleUpdate,
|
||||||
|
handleShuffle,
|
||||||
|
handleShare,
|
||||||
|
isZen,
|
||||||
|
setIsZen,
|
||||||
|
} = useContext(AppContext);
|
||||||
|
return (
|
||||||
|
<header
|
||||||
|
id="header"
|
||||||
|
className={cx(
|
||||||
|
'flex-none w-full md:flex text-black justify-between z-[100] text-lg select-none sticky top-0',
|
||||||
|
isEmbedded ? 'h-12 md:h-8' : 'h-25 md:h-14',
|
||||||
|
!isZen && 'bg-header',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="px-4 flex items-center space-x-2 pt-2 md:pt-0 select-none">
|
||||||
|
{/* <img
|
||||||
|
src={logo}
|
||||||
|
className={cx('Tidal-logo', isEmbedded ? 'w-8 h-8' : 'w-10 h-10', started && 'animate-pulse')} // 'bg-[#ffffff80] rounded-full'
|
||||||
|
alt="logo"
|
||||||
|
/> */}
|
||||||
|
<h1
|
||||||
|
className={cx(
|
||||||
|
isEmbedded ? 'text-l' : 'text-xl',
|
||||||
|
// 'bg-clip-text bg-gradient-to-r from-primary to-secondary text-transparent font-bold',
|
||||||
|
'text-white font-bold flex space-x-2',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cx('mt-[1px]', started && 'animate-spin', 'cursor-pointer')}
|
||||||
|
onClick={() => setIsZen((z) => !z)}
|
||||||
|
>
|
||||||
|
🌀
|
||||||
|
</div>
|
||||||
|
{!isZen && (
|
||||||
|
<div className={cx(started && 'animate-pulse')}>
|
||||||
|
<span className="">strudel</span> <span className="text-sm">REPL</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
{!isZen && (
|
||||||
|
<div className="flex max-w-full overflow-auto text-white ">
|
||||||
|
<button
|
||||||
|
onClick={handleTogglePlay}
|
||||||
|
className={cx(!isEmbedded ? 'p-2' : 'px-2', 'hover:text-tertiary', !started && 'animate-pulse')}
|
||||||
|
>
|
||||||
|
{!pending ? (
|
||||||
|
<span className={cx('flex items-center space-x-1', isEmbedded ? 'w-16' : 'w-16')}>
|
||||||
|
{started ? <StopCircleIcon className="w-5 h-5" /> : <PlayCircleIcon className="w-5 h-5" />}
|
||||||
|
<span>{started ? 'stop' : 'play'}</span>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<>loading...</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleUpdate}
|
||||||
|
className={cx(
|
||||||
|
'flex items-center space-x-1',
|
||||||
|
!isEmbedded ? 'p-2' : 'px-2',
|
||||||
|
!isDirty || !activeCode ? 'opacity-50' : 'hover:text-tertiary',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<CommandLineIcon className="w-5 h-5" />
|
||||||
|
<span>update</span>
|
||||||
|
</button>
|
||||||
|
{!isEmbedded && (
|
||||||
|
<button className="hover:text-tertiary p-2 flex items-center space-x-1" onClick={handleShuffle}>
|
||||||
|
<SparklesIcon className="w-5 h-5" />
|
||||||
|
<span> shuffle</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{!isEmbedded && (
|
||||||
|
<a
|
||||||
|
href="./tutorial"
|
||||||
|
className={cx('hover:text-tertiary flex items-center space-x-1', !isEmbedded ? 'p-2' : 'px-2')}
|
||||||
|
>
|
||||||
|
<AcademicCapIcon className="w-5 h-5" />
|
||||||
|
<span>learn</span>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
{!isEmbedded && (
|
||||||
|
<button
|
||||||
|
className={cx(
|
||||||
|
'cursor-pointer hover:text-tertiary flex items-center space-x-1',
|
||||||
|
!isEmbedded ? 'p-2' : 'px-2',
|
||||||
|
)}
|
||||||
|
onClick={handleShare}
|
||||||
|
>
|
||||||
|
<LinkIcon className="w-5 h-5" />
|
||||||
|
<span>share{lastShared && lastShared === (activeCode || code) ? 'd!' : ''}</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{isEmbedded && (
|
||||||
|
<button className={cx('hover:text-tertiary px-2')}>
|
||||||
|
<a href={window.location.href} target="_blank" rel="noopener noreferrer" title="Open in REPL">
|
||||||
|
🚀 open
|
||||||
|
</a>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{isEmbedded && (
|
||||||
|
<button className={cx('hover:text-tertiary px-2')}>
|
||||||
|
<a
|
||||||
|
onClick={() => {
|
||||||
|
window.location.href = initialUrl;
|
||||||
|
window.location.reload();
|
||||||
|
}}
|
||||||
|
title="Reset"
|
||||||
|
>
|
||||||
|
💔 reset
|
||||||
|
</a>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
38
repl/src/Reference.jsx
Normal file
38
repl/src/Reference.jsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import jsdocJson from '../../doc.json';
|
||||||
|
const visibleFunctions = jsdocJson.docs
|
||||||
|
.filter(({ name, description }) => name && !name.startsWith('_') && !!description)
|
||||||
|
.sort((a, b) => a.meta.filename.localeCompare(b.meta.filename) + a.name.localeCompare(b.name));
|
||||||
|
|
||||||
|
export function Reference() {
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full pt-2">
|
||||||
|
<div className="w-64 flex-none h-full overflow-y-auto overflow-x-hidden pr-4">
|
||||||
|
{visibleFunctions.map((entry, i) => (
|
||||||
|
<a key={i} className="cursor-pointer block hover:bg-linegray py-1 px-4" href={`#doc-${i}`}>
|
||||||
|
{entry.name} {/* <span className="text-gray-600">{entry.meta.filename}</span> */}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="break-normal w-full h-full overflow-auto pl-4 flex relative">
|
||||||
|
<div className="prose prose-invert">
|
||||||
|
<h2>API Reference</h2>
|
||||||
|
<p>
|
||||||
|
This is the long list functions you can use! Remember that you don't need to remember all of those and that
|
||||||
|
you can already make music with a small set of functions!
|
||||||
|
</p>
|
||||||
|
{visibleFunctions.map((entry, i) => (
|
||||||
|
<section key={i}>
|
||||||
|
<h3 id={`doc-${i}`}>{entry.name}</h3>
|
||||||
|
{/* <small>{entry.meta.filename}</small> */}
|
||||||
|
|
||||||
|
<p dangerouslySetInnerHTML={{ __html: entry.description }}></p>
|
||||||
|
{entry.examples?.map((example, j) => (
|
||||||
|
<pre key={j}>{example}</pre>
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -5,13 +5,15 @@ export async function prebake({ isMock = false, baseDir = '.' } = {}) {
|
|||||||
if (!isMock) {
|
if (!isMock) {
|
||||||
// https://archive.org/details/SalamanderGrandPianoV3
|
// https://archive.org/details/SalamanderGrandPianoV3
|
||||||
// License: CC-by http://creativecommons.org/licenses/by/3.0/ Author: Alexander Holm
|
// License: CC-by http://creativecommons.org/licenses/by/3.0/ Author: Alexander Holm
|
||||||
samples('piano.json', `${baseDir}/piano/`);
|
return await Promise.all([
|
||||||
// https://github.com/sgossner/VCSL/
|
samples('piano.json', `${baseDir}/piano/`),
|
||||||
// https://api.github.com/repositories/126427031/contents/
|
// https://github.com/sgossner/VCSL/
|
||||||
// LICENSE: CC0 general-purpose
|
// https://api.github.com/repositories/126427031/contents/
|
||||||
samples('vcsl.json', 'github:sgossner/VCSL/master/');
|
// LICENSE: CC0 general-purpose
|
||||||
samples('tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/');
|
samples('vcsl.json', 'github:sgossner/VCSL/master/'),
|
||||||
samples('EmuSP12.json', `${baseDir}/EmuSP12/`);
|
samples('tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/'),
|
||||||
|
samples('EmuSP12.json', `${baseDir}/EmuSP12/`),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,8 +3,8 @@
|
|||||||
// it might require mocking more stuff when tunes added that use other functions
|
// it might require mocking more stuff when tunes added that use other functions
|
||||||
|
|
||||||
// import * as tunes from './tunes.mjs';
|
// import * as tunes from './tunes.mjs';
|
||||||
import { evaluate } from '@strudel.cycles/eval';
|
// import { evaluate } from '@strudel.cycles/eval';
|
||||||
// import { evaluate } from '@strudel.cycles/transpiler';
|
import { evaluate } from '@strudel.cycles/transpiler';
|
||||||
import { evalScope } from '@strudel.cycles/core';
|
import { evalScope } from '@strudel.cycles/core';
|
||||||
import * as strudel from '@strudel.cycles/core';
|
import * as strudel from '@strudel.cycles/core';
|
||||||
import * as webaudio from '@strudel.cycles/webaudio';
|
import * as webaudio from '@strudel.cycles/webaudio';
|
||||||
|
|||||||
@ -8335,6 +8335,21 @@ exports[`renders tunes > tune: hyperpop 1`] = `
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`renders tunes > tune: juxUndTollerei 1`] = `
|
||||||
|
[
|
||||||
|
"3/4 -> 1/1: {\\"note\\":\\"c3\\",\\"s\\":\\"sawtooth\\",\\"pan\\":0,\\"cutoff\\":1670.953955747281,\\"decay\\":0.05,\\"sustain\\":0,\\"room\\":0.6,\\"delay\\":0.5,\\"delaytime\\":0.1,\\"delayfeedback\\":0.4}",
|
||||||
|
"1/2 -> 3/4: {\\"note\\":\\"eb3\\",\\"s\\":\\"sawtooth\\",\\"pan\\":0,\\"cutoff\\":1524.257063143398,\\"decay\\":0.05,\\"sustain\\":0,\\"room\\":0.6,\\"delay\\":0.5,\\"delaytime\\":0.1,\\"delayfeedback\\":0.4}",
|
||||||
|
"1/4 -> 1/2: {\\"note\\":\\"g3\\",\\"s\\":\\"sawtooth\\",\\"pan\\":0,\\"cutoff\\":1361.2562095290161,\\"decay\\":0.05,\\"sustain\\":0,\\"room\\":0.6,\\"delay\\":0.5,\\"delaytime\\":0.1,\\"delayfeedback\\":0.4}",
|
||||||
|
"0/1 -> 1/4: {\\"note\\":\\"bb3\\",\\"s\\":\\"sawtooth\\",\\"pan\\":0,\\"cutoff\\":1188.2154262966046,\\"decay\\":0.05,\\"sustain\\":0,\\"room\\":0.6,\\"delay\\":0.5,\\"delaytime\\":0.1,\\"delayfeedback\\":0.4}",
|
||||||
|
"0/1 -> 1/4: {\\"note\\":\\"c3\\",\\"s\\":\\"sawtooth\\",\\"pan\\":1,\\"cutoff\\":1188.2154262966046,\\"decay\\":0.05,\\"sustain\\":0,\\"room\\":0.6,\\"delay\\":0.5,\\"delaytime\\":0.1,\\"delayfeedback\\":0.4}",
|
||||||
|
"1/4 -> 1/2: {\\"note\\":\\"eb3\\",\\"s\\":\\"sawtooth\\",\\"pan\\":1,\\"cutoff\\":1361.2562095290161,\\"decay\\":0.05,\\"sustain\\":0,\\"room\\":0.6,\\"delay\\":0.5,\\"delaytime\\":0.1,\\"delayfeedback\\":0.4}",
|
||||||
|
"1/2 -> 3/4: {\\"note\\":\\"g3\\",\\"s\\":\\"sawtooth\\",\\"pan\\":1,\\"cutoff\\":1524.257063143398,\\"decay\\":0.05,\\"sustain\\":0,\\"room\\":0.6,\\"delay\\":0.5,\\"delaytime\\":0.1,\\"delayfeedback\\":0.4}",
|
||||||
|
"3/4 -> 1/1: {\\"note\\":\\"bb3\\",\\"s\\":\\"sawtooth\\",\\"pan\\":1,\\"cutoff\\":1670.953955747281,\\"decay\\":0.05,\\"sustain\\":0,\\"room\\":0.6,\\"delay\\":0.5,\\"delaytime\\":0.1,\\"delayfeedback\\":0.4}",
|
||||||
|
"101/200 -> 201/200: {\\"note\\":65,\\"s\\":\\"triangle\\",\\"pan\\":0,\\"cutoff\\":1601.4815730092653,\\"decay\\":0.05,\\"sustain\\":0,\\"room\\":0.6,\\"delay\\":0.5,\\"delaytime\\":0.1,\\"delayfeedback\\":0.4}",
|
||||||
|
"101/200 -> 201/200: {\\"note\\":55,\\"s\\":\\"triangle\\",\\"pan\\":1,\\"cutoff\\":1601.4815730092653,\\"decay\\":0.05,\\"sustain\\":0,\\"room\\":0.6,\\"delay\\":0.5,\\"delaytime\\":0.1,\\"delayfeedback\\":0.4}",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`renders tunes > tune: meltingsubmarine 1`] = `
|
exports[`renders tunes > tune: meltingsubmarine 1`] = `
|
||||||
[
|
[
|
||||||
"0/1 -> 3/2: {\\"s\\":\\"bd\\",\\"speed\\":0.7519542165100574}",
|
"0/1 -> 3/2: {\\"s\\":\\"bd\\",\\"speed\\":0.7519542165100574}",
|
||||||
|
|||||||
@ -308,7 +308,6 @@ export const blippyRhodes = `samples({
|
|||||||
}
|
}
|
||||||
}, 'https://loophole-letters.vercel.app/')
|
}, 'https://loophole-letters.vercel.app/')
|
||||||
|
|
||||||
const bass = synth(osc('sawtooth8')).chain(vol(.5),out())
|
|
||||||
const scales = cat('C major', 'C mixolydian', 'F lydian', ['F minor', cat('Db major','Db mixolydian')])
|
const scales = cat('C major', 'C mixolydian', 'F lydian', ['F minor', cat('Db major','Db mixolydian')])
|
||||||
|
|
||||||
stack(
|
stack(
|
||||||
@ -950,7 +949,7 @@ export const flatrave = `stack(
|
|||||||
.decay(.05).sustain(0).delay(.2).degradeBy(.5).mask("<0 1>/16")
|
.decay(.05).sustain(0).delay(.2).degradeBy(.5).mask("<0 1>/16")
|
||||||
)`;
|
)`;
|
||||||
|
|
||||||
export const amensister = `samples('github:tidalcycles/Dirt-Samples/master')
|
export const amensister = `await samples('github:tidalcycles/Dirt-Samples/master')
|
||||||
|
|
||||||
stack(
|
stack(
|
||||||
// amen
|
// amen
|
||||||
@ -981,3 +980,14 @@ stack(
|
|||||||
,
|
,
|
||||||
n("0 1").s("east").delay(.5).degradeBy(.8).speed(rand.range(.5,1.5))
|
n("0 1").s("east").delay(.5).degradeBy(.8).speed(rand.range(.5,1.5))
|
||||||
).reset("<x@7 x(5,8)>")`;
|
).reset("<x@7 x(5,8)>")`;
|
||||||
|
|
||||||
|
export const juxUndTollerei = `note("c3 eb3 g3 bb3").palindrome()
|
||||||
|
.s('sawtooth')
|
||||||
|
.jux(x=>x.rev().color('green').s('sawtooth'))
|
||||||
|
.off(1/4, x=>x.add(note("<7 12>/2")).slow(2).late(.005).s('triangle'))
|
||||||
|
//.delay(.5)
|
||||||
|
.fast(1).cutoff(sine.range(200,2000).slow(8))
|
||||||
|
.decay(.05).sustain(0)
|
||||||
|
.room(.6)
|
||||||
|
.delay(.5).delaytime(.1).delayfeedback(.4)
|
||||||
|
.pianoroll()`;
|
||||||
|
|||||||
@ -6,9 +6,24 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
// TODO: find out if leaving out tutorial path works now
|
// TODO: find out if leaving out tutorial path works now
|
||||||
content: ['./src/**/*.{js,jsx,ts,tsx}','./tutorial/**/*.{js,jsx,ts,tsx}'],
|
content: ['./src/**/*.{js,jsx,ts,tsx}', './tutorial/**/*.{js,jsx,ts,tsx}'],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {},
|
extend: {
|
||||||
|
colors: {
|
||||||
|
primary: '#c792ea',
|
||||||
|
secondary: '#c3e88d',
|
||||||
|
tertiary: '#82aaff',
|
||||||
|
highlight: '#ffcc00',
|
||||||
|
linegray: '#8a91991a',
|
||||||
|
lineblack: '#00000095',
|
||||||
|
bg: '#222222',
|
||||||
|
// header: '#8a91991a',
|
||||||
|
// footer: '#8a91991a',
|
||||||
|
header: '#00000050',
|
||||||
|
// header: 'transparent',
|
||||||
|
footer: '#00000050',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
plugins: [require('@tailwindcss/typography')],
|
plugins: [require('@tailwindcss/typography')],
|
||||||
};
|
};
|
||||||
|
|||||||
@ -7,7 +7,7 @@ export default defineConfig({
|
|||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
build: {
|
build: {
|
||||||
outDir: '../out',
|
outDir: '../out',
|
||||||
sourcemap: false,
|
sourcemap: true,
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
plugins: [visualizer({ template: 'treemap' })],
|
plugins: [visualizer({ template: 'treemap' })],
|
||||||
},
|
},
|
||||||
|
|||||||
@ -10,7 +10,7 @@ fetch('https://strudel.tidalcycles.org/EmuSP12.json')
|
|||||||
evalScope(
|
evalScope(
|
||||||
controls,
|
controls,
|
||||||
import('@strudel.cycles/core'),
|
import('@strudel.cycles/core'),
|
||||||
import('@strudel.cycles/tone'),
|
// import('@strudel.cycles/tone'),
|
||||||
import('@strudel.cycles/tonal'),
|
import('@strudel.cycles/tonal'),
|
||||||
import('@strudel.cycles/mini'),
|
import('@strudel.cycles/mini'),
|
||||||
import('@strudel.cycles/midi'),
|
import('@strudel.cycles/midi'),
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user