mirror of
https://github.com/eliasstepanik/strudel.git
synced 2026-01-12 06:08:37 +00:00
Merge pull request #255 from tidalcycles/repl-refactoring
Repl refactoring
This commit is contained in:
commit
4bc2f64b24
@ -1,7 +1,7 @@
|
||||
{
|
||||
"source": {
|
||||
"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"],
|
||||
"opts": {
|
||||
|
||||
41
package-lock.json
generated
41
package-lock.json
generated
@ -12495,10 +12495,10 @@
|
||||
},
|
||||
"packages/midi": {
|
||||
"name": "@strudel.cycles/midi",
|
||||
"version": "0.3.2",
|
||||
"version": "0.3.3",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@strudel.cycles/tone": "^0.3.2",
|
||||
"@strudel.cycles/tone": "^0.3.3",
|
||||
"tone": "^14.7.77",
|
||||
"webmidi": "^3.0.21"
|
||||
}
|
||||
@ -12519,12 +12519,12 @@
|
||||
},
|
||||
"packages/mini": {
|
||||
"name": "@strudel.cycles/mini",
|
||||
"version": "0.3.2",
|
||||
"version": "0.3.3",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@strudel.cycles/core": "^0.3.2",
|
||||
"@strudel.cycles/eval": "^0.3.2",
|
||||
"@strudel.cycles/tone": "^0.3.2"
|
||||
"@strudel.cycles/tone": "^0.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"peggy": "^2.0.1"
|
||||
@ -12540,13 +12540,14 @@
|
||||
},
|
||||
"packages/react": {
|
||||
"name": "@strudel.cycles/react",
|
||||
"version": "0.3.2",
|
||||
"version": "0.3.3",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@codemirror/lang-javascript": "^6.1.1",
|
||||
"@strudel.cycles/core": "^0.3.2",
|
||||
"@strudel.cycles/eval": "^0.3.2",
|
||||
"@strudel.cycles/tone": "^0.3.2",
|
||||
"@strudel.cycles/tone": "^0.3.3",
|
||||
"@strudel.cycles/transpiler": "^0.3.2",
|
||||
"@strudel.cycles/webaudio": "^0.3.3",
|
||||
"@uiw/codemirror-themes": "^4.12.4",
|
||||
"@uiw/react-codemirror": "^4.12.4",
|
||||
"react-hook-inview": "^4.5.0"
|
||||
@ -12621,11 +12622,11 @@
|
||||
},
|
||||
"packages/soundfonts": {
|
||||
"name": "@strudel.cycles/soundfonts",
|
||||
"version": "0.3.2",
|
||||
"version": "0.3.3",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@strudel.cycles/core": "^0.3.2",
|
||||
"@strudel.cycles/webaudio": "^0.3.2",
|
||||
"@strudel.cycles/webaudio": "^0.3.3",
|
||||
"sfumato": "^0.1.2",
|
||||
"soundfont2": "^0.4.0"
|
||||
},
|
||||
@ -12653,7 +12654,7 @@
|
||||
},
|
||||
"packages/tonal": {
|
||||
"name": "@strudel.cycles/tonal",
|
||||
"version": "0.3.2",
|
||||
"version": "0.3.3",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@strudel.cycles/core": "^0.3.2",
|
||||
@ -12678,7 +12679,7 @@
|
||||
},
|
||||
"packages/tone": {
|
||||
"name": "@strudel.cycles/tone",
|
||||
"version": "0.3.2",
|
||||
"version": "0.3.3",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@strudel.cycles/core": "^0.3.2",
|
||||
@ -12709,7 +12710,7 @@
|
||||
},
|
||||
"packages/webaudio": {
|
||||
"name": "@strudel.cycles/webaudio",
|
||||
"version": "0.3.2",
|
||||
"version": "0.3.3",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@strudel.cycles/core": "^0.3.2"
|
||||
@ -14427,7 +14428,7 @@
|
||||
"@strudel.cycles/midi": {
|
||||
"version": "file:packages/midi",
|
||||
"requires": {
|
||||
"@strudel.cycles/tone": "^0.3.2",
|
||||
"@strudel.cycles/tone": "^0.3.3",
|
||||
"tone": "^14.7.77",
|
||||
"webmidi": "^3.0.21"
|
||||
},
|
||||
@ -14448,7 +14449,7 @@
|
||||
"requires": {
|
||||
"@strudel.cycles/core": "^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"
|
||||
}
|
||||
},
|
||||
@ -14463,8 +14464,9 @@
|
||||
"requires": {
|
||||
"@codemirror/lang-javascript": "^6.1.1",
|
||||
"@strudel.cycles/core": "^0.3.2",
|
||||
"@strudel.cycles/eval": "^0.3.2",
|
||||
"@strudel.cycles/tone": "^0.3.2",
|
||||
"@strudel.cycles/tone": "^0.3.3",
|
||||
"@strudel.cycles/transpiler": "^0.3.2",
|
||||
"@strudel.cycles/webaudio": "^0.3.3",
|
||||
"@types/react": "^17.0.2",
|
||||
"@types/react-dom": "^17.0.2",
|
||||
"@uiw/codemirror-themes": "^4.12.4",
|
||||
@ -14520,7 +14522,7 @@
|
||||
"version": "file:packages/soundfonts",
|
||||
"requires": {
|
||||
"@strudel.cycles/core": "^0.3.2",
|
||||
"@strudel.cycles/webaudio": "^0.3.2",
|
||||
"@strudel.cycles/webaudio": "^0.3.3",
|
||||
"node-fetch": "^3.2.6",
|
||||
"sfumato": "^0.1.2",
|
||||
"soundfont2": "^0.4.0"
|
||||
@ -20343,8 +20345,9 @@
|
||||
"requires": {
|
||||
"@codemirror/lang-javascript": "^6.1.1",
|
||||
"@strudel.cycles/core": "^0.3.2",
|
||||
"@strudel.cycles/eval": "^0.3.2",
|
||||
"@strudel.cycles/tone": "^0.3.2",
|
||||
"@strudel.cycles/tone": "^0.3.3",
|
||||
"@strudel.cycles/transpiler": "^0.3.2",
|
||||
"@strudel.cycles/webaudio": "^0.3.3",
|
||||
"@types/react": "^17.0.2",
|
||||
"@types/react-dom": "^17.0.2",
|
||||
"@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 { logger } from './logger.mjs';
|
||||
|
||||
export class Cyclist {
|
||||
worker;
|
||||
@ -13,8 +14,10 @@ export class Cyclist {
|
||||
cps = 1; // TODO
|
||||
getTime;
|
||||
phase = 0;
|
||||
constructor({ interval, onTrigger, onError, getTime, latency = 0.1 }) {
|
||||
constructor({ interval, onTrigger, onToggle, onError, getTime, latency = 0.1 }) {
|
||||
this.getTime = getTime;
|
||||
this.onToggle = onToggle;
|
||||
this.latency = latency;
|
||||
const round = (x) => Math.round(x * 1000) / 1000;
|
||||
this.clock = createClock(
|
||||
getTime,
|
||||
@ -28,9 +31,7 @@ export class Cyclist {
|
||||
const time = getTime();
|
||||
try {
|
||||
const haps = this.pattern.queryArc(begin, end); // get Haps
|
||||
// console.log('haps', haps.map((hap) => hap.value.n).join(' '));
|
||||
haps.forEach((hap) => {
|
||||
// console.log('hap', hap.value.n, hap.part.begin);
|
||||
if (hap.part.begin.equals(hap.whole.begin)) {
|
||||
const deadline = hap.whole.begin + this.origin - time + latency;
|
||||
const duration = hap.duration * 1;
|
||||
@ -38,7 +39,7 @@ export class Cyclist {
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn('scheduler error', e);
|
||||
logger(`[cyclist] error: ${e.message}`);
|
||||
onError?.(e);
|
||||
}
|
||||
}, // called slightly before each cycle
|
||||
@ -46,24 +47,29 @@ export class Cyclist {
|
||||
);
|
||||
}
|
||||
getPhase() {
|
||||
return this.phase;
|
||||
return this.getTime() - this.origin - this.latency;
|
||||
}
|
||||
setStarted(v) {
|
||||
this.started = v;
|
||||
this.onToggle?.(v);
|
||||
}
|
||||
start() {
|
||||
if (!this.pattern) {
|
||||
throw new Error('Scheduler: no pattern set! call .setPattern first.');
|
||||
}
|
||||
logger('[cyclist] start');
|
||||
this.clock.start();
|
||||
this.started = true;
|
||||
this.setStarted(true);
|
||||
}
|
||||
pause() {
|
||||
this.clock.stop();
|
||||
delete this.origin;
|
||||
this.started = false;
|
||||
logger('[cyclist] pause');
|
||||
this.clock.pause();
|
||||
this.setStarted(false);
|
||||
}
|
||||
stop() {
|
||||
delete this.origin;
|
||||
logger('[cyclist] stop');
|
||||
this.clock.stop();
|
||||
this.started = false;
|
||||
this.setStarted(false);
|
||||
}
|
||||
setPattern(pat, autostart = false) {
|
||||
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/>.
|
||||
*/
|
||||
|
||||
import { Tone } from './tone.mjs';
|
||||
import { Pattern } from '@strudel.cycles/core';
|
||||
import { Pattern, getTime } from './index.mjs';
|
||||
|
||||
export const getDrawContext = (id = 'test-canvas') => {
|
||||
let canvas = document.querySelector('#' + id);
|
||||
@ -28,7 +27,7 @@ Pattern.prototype.draw = function (callback, { from, to, onQuery }) {
|
||||
let cycle,
|
||||
events = [];
|
||||
const animate = (time) => {
|
||||
const t = Tone.getTransport().seconds;
|
||||
const t = getTime();
|
||||
if (from !== undefined && to !== undefined) {
|
||||
const currentCycle = Math.floor(t);
|
||||
if (cycle !== currentCycle) {
|
||||
@ -50,9 +49,9 @@ Pattern.prototype.draw = function (callback, { from, to, onQuery }) {
|
||||
return this;
|
||||
};
|
||||
|
||||
export const cleanupDraw = () => {
|
||||
export const cleanupDraw = (clearScreen = true) => {
|
||||
const ctx = getDrawContext();
|
||||
ctx.clearRect(0, 0, window.innerWidth, window.innerHeight);
|
||||
clearScreen && ctx.clearRect(0, 0, window.innerWidth, window.innerHeight);
|
||||
if (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/>.
|
||||
*/
|
||||
|
||||
import * as strudel from '@strudel.cycles/core';
|
||||
|
||||
const { isPattern, Pattern } = strudel;
|
||||
import { isPattern, Pattern } from './index.mjs';
|
||||
|
||||
let scoped = false;
|
||||
export const evalScope = async (...args) => {
|
||||
|
||||
@ -55,7 +55,8 @@ Fraction.prototype.min = function (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;
|
||||
};
|
||||
|
||||
|
||||
@ -85,9 +85,13 @@ export class Hap {
|
||||
);
|
||||
}
|
||||
|
||||
showWhole() {
|
||||
showWhole(compact = false) {
|
||||
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';
|
||||
export * from './euclid.mjs';
|
||||
import Fraction from './fraction.mjs';
|
||||
import { logger } from './logger.mjs';
|
||||
export { Fraction, controls };
|
||||
export * from './hap.mjs';
|
||||
export * from './pattern.mjs';
|
||||
@ -17,15 +18,17 @@ export * from './util.mjs';
|
||||
export * from './speak.mjs';
|
||||
export * from './evaluate.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 gist } from './gist.js';
|
||||
// below won't work with runtime.mjs (json import fails)
|
||||
/* import * as p from './package.json';
|
||||
export const version = p.version; */
|
||||
console.log(
|
||||
'%c // 🌀 @strudel.cycles/core loaded 🌀', // keep "//" for runnable snapshot source..
|
||||
'background-color: black;color:white;padding:4px;border-radius:15px',
|
||||
);
|
||||
logger('🌀 @strudel.cycles/core loaded 🌀');
|
||||
if (globalThis._strudelLoaded) {
|
||||
console.warn(
|
||||
`@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 drawLine from './drawLine.mjs';
|
||||
import { logger } from './logger.mjs';
|
||||
|
||||
let stringParser;
|
||||
// parser is expected to turn a string into a pattern
|
||||
@ -1328,22 +1329,25 @@ export class Pattern {
|
||||
.unit('c')
|
||||
.slow(factor);
|
||||
}
|
||||
onTrigger(onTrigger) {
|
||||
return this._withHap((hap) => hap.setContext({ ...hap.context, onTrigger }));
|
||||
}
|
||||
log(func = id) {
|
||||
onTrigger(onTrigger, dominant = true) {
|
||||
return this._withHap((hap) =>
|
||||
hap.setContext({
|
||||
...hap.context,
|
||||
onTrigger: (...args) => {
|
||||
if (hap.context.onTrigger) {
|
||||
if (!dominant && hap.context.onTrigger) {
|
||||
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) {
|
||||
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/>.
|
||||
*/
|
||||
|
||||
import { Pattern } from '@strudel.cycles/core';
|
||||
import { Pattern } from './index.mjs';
|
||||
|
||||
const scale = (normalized, min, max) => normalized * (max - min) + min;
|
||||
const getValue = (e) => {
|
||||
@ -1,23 +1,59 @@
|
||||
import { Cyclist } from './cyclist.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 }) {
|
||||
const scheduler = new Cyclist({ interval, onTrigger: defaultOutput, onError: onSchedulerError, getTime });
|
||||
const evaluate = async (code) => {
|
||||
export function repl({
|
||||
interval,
|
||||
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) {
|
||||
throw new Error('no code to evaluate');
|
||||
}
|
||||
try {
|
||||
beforeEval({ code });
|
||||
const { pattern } = await _evaluate(code, transpiler);
|
||||
scheduler.setPattern(pattern, true);
|
||||
onEval?.({
|
||||
pattern,
|
||||
code,
|
||||
});
|
||||
logger(`[eval] code updated`);
|
||||
scheduler.setPattern(pattern, autostart);
|
||||
afterEval({ code, pattern });
|
||||
return pattern;
|
||||
} catch (err) {
|
||||
console.warn(`eval error: ${err.message}`);
|
||||
// console.warn(`[repl] eval error: ${err.message}`);
|
||||
logger(`[eval] error: ${err.message}`, 'error');
|
||||
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) {
|
||||
return this._withHap((hap) => {
|
||||
const onTrigger = (time, hap) => {
|
||||
speak(hap.value, lang, voice);
|
||||
};
|
||||
return hap.setContext({ ...hap.context, onTrigger });
|
||||
return this.onTrigger((_, hap) => {
|
||||
speak(hap.value, lang, voice);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
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/>.
|
||||
*/
|
||||
|
||||
import { Tone } from './tone.mjs';
|
||||
|
||||
export const hideHeader = () => {
|
||||
document.getElementById('header').style = 'display:none';
|
||||
};
|
||||
import { getTime } from './time.mjs';
|
||||
|
||||
function frame(callback) {
|
||||
if (window.strudelAnimation) {
|
||||
cancelAnimationFrame(window.strudelAnimation);
|
||||
}
|
||||
const animate = (animationTime) => {
|
||||
const toneTime = Tone.getTransport().seconds;
|
||||
callback(animationTime, toneTime);
|
||||
callback(animationTime, getTime());
|
||||
window.strudelAnimation = requestAnimationFrame(animate);
|
||||
};
|
||||
requestAnimationFrame(animate);
|
||||
@ -51,6 +46,7 @@ export const cleanupUi = () => {
|
||||
const container = document.getElementById('code');
|
||||
if (container) {
|
||||
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 getPlayableNoteValue = (hap) => {
|
||||
let { value: note, context } = hap;
|
||||
let { value, context } = hap;
|
||||
let note = value;
|
||||
if (typeof note === 'object' && !Array.isArray(note)) {
|
||||
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 (typeof note === 'number' && context.type !== 'frequency') {
|
||||
|
||||
@ -31,10 +31,11 @@ function createClock(
|
||||
};
|
||||
let intervalID;
|
||||
const start = () => {
|
||||
clear(); // just in case start was called more than once
|
||||
onTick();
|
||||
intervalID = setInterval(onTick, interval * 1000);
|
||||
};
|
||||
const clear = () => clearInterval(intervalID);
|
||||
const clear = () => intervalID !== undefined && clearInterval(intervalID);
|
||||
const pause = () => clear();
|
||||
const stop = () => {
|
||||
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/>.
|
||||
*/
|
||||
|
||||
import { isNote } from 'tone';
|
||||
import * as _WebMidi from 'webmidi';
|
||||
import { Pattern, isPattern } from '@strudel.cycles/core';
|
||||
import { Tone } from '@strudel.cycles/tone';
|
||||
import { Pattern, isPattern, isNote, getPlayableNoteValue, logger } from '@strudel.cycles/core';
|
||||
import { getAudioContext } from '@strudel.cycles/webaudio';
|
||||
|
||||
// if you use WebMidi from outside of this package, make sure to import that instance:
|
||||
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) => {
|
||||
if (WebMidi.enabled) {
|
||||
// if already enabled, just resolve WebMidi
|
||||
@ -22,6 +27,14 @@ export function enableWebMidi() {
|
||||
if (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);
|
||||
});
|
||||
});
|
||||
@ -30,7 +43,21 @@ export function enableWebMidi() {
|
||||
const outputByName = (name) => WebMidi.getOutputByName(name);
|
||||
|
||||
// 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)) {
|
||||
throw new Error(
|
||||
`.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) => {
|
||||
// const onTrigger = (time: number, hap: any) => {
|
||||
const onTrigger = (time, hap) => {
|
||||
let note = hap.value;
|
||||
const velocity = hap.context?.velocity ?? 0.9;
|
||||
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.outputs.length) {
|
||||
throw new Error(`🔌 No MIDI devices found. Connect a device or enable IAC Driver.`);
|
||||
}
|
||||
let device;
|
||||
if (typeof output === 'number') {
|
||||
device = WebMidi.outputs[output];
|
||||
} else if (typeof output === 'string') {
|
||||
device = outputByName(output);
|
||||
} else {
|
||||
device = WebMidi.outputs[0];
|
||||
}
|
||||
if (!device) {
|
||||
throw new Error(
|
||||
`🔌 MIDI device '${output ? output : ''}' not found. Use one of ${WebMidi.outputs
|
||||
.map((o) => `'${o.name}'`)
|
||||
.join(' | ')}`,
|
||||
);
|
||||
}
|
||||
// console.log('midi', value, output);
|
||||
const timingOffset = WebMidi.time - Tone.getContext().currentTime * 1000;
|
||||
time = time * 1000 + timingOffset;
|
||||
// const inMs = '+' + (time - Tone.getContext().currentTime) * 1000;
|
||||
// await enableWebMidi()
|
||||
device.playNote(note, channel, {
|
||||
time,
|
||||
duration: hap.duration.valueOf() * 1000 - 5,
|
||||
velocity,
|
||||
});
|
||||
};
|
||||
return hap.setContext({ ...hap.context, onTrigger });
|
||||
return this.onTrigger((time, hap) => {
|
||||
let note = getPlayableNoteValue(hap);
|
||||
const velocity = hap.context?.velocity ?? 0.9;
|
||||
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.outputs.length) {
|
||||
throw new Error(`🔌 No MIDI devices found. Connect a device or enable IAC Driver.`);
|
||||
}
|
||||
let device;
|
||||
if (typeof output === 'number') {
|
||||
device = WebMidi.outputs[output];
|
||||
} else if (typeof output === 'string') {
|
||||
device = outputByName(output);
|
||||
} else {
|
||||
device = WebMidi.outputs[0];
|
||||
}
|
||||
if (!device) {
|
||||
throw new Error(
|
||||
`🔌 MIDI device '${output ? output : ''}' not found. Use one of ${WebMidi.outputs
|
||||
.map((o) => `'${o.name}'`)
|
||||
.join(' | ')}`,
|
||||
);
|
||||
}
|
||||
// console.log('midi', value, output);
|
||||
const timingOffset = WebMidi.time - getAudioContext().currentTime * 1000;
|
||||
time = time * 1000 + timingOffset;
|
||||
// const inMs = '+' + (time - Tone.getContext().currentTime) * 1000;
|
||||
// await enableWebMidi()
|
||||
device.playNote(note, channel, {
|
||||
time,
|
||||
duration: hap.duration.valueOf() * 1000 - 5,
|
||||
attack: velocity,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@ -32,7 +32,7 @@ function peg$padEnd(str, targetLength, padString) {
|
||||
}
|
||||
|
||||
peg$SyntaxError.prototype.format = function(sources) {
|
||||
var str = "Error: " + this.message;
|
||||
var str = "peg error: " + this.message;
|
||||
if (this.location) {
|
||||
var src = null;
|
||||
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 { 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;
|
||||
let startedAt = -1;
|
||||
|
||||
@ -20,28 +44,25 @@ let startedAt = -1;
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
*/
|
||||
Pattern.prototype.osc = function () {
|
||||
return this._withHap((hap) => {
|
||||
const onTrigger = (time, hap, currentTime, cps) => {
|
||||
const cycle = hap.wholeOrPart().begin.valueOf();
|
||||
const delta = hap.duration.valueOf();
|
||||
// time should be audio time of onset
|
||||
// currentTime should be current time of audio context (slightly before time)
|
||||
if (startedAt < 0) {
|
||||
startedAt = Date.now() - currentTime * 1000;
|
||||
}
|
||||
const controls = Object.assign({}, { cps, cycle, delta }, hap.value);
|
||||
// make sure n and note are numbers
|
||||
controls.n && (controls.n = parseNumeral(controls.n));
|
||||
controls.note && (controls.note = parseNumeral(controls.note));
|
||||
|
||||
const keyvals = Object.entries(controls).flat();
|
||||
const ts = Math.floor(startedAt + (time + latency) * 1000);
|
||||
const message = new OSC.Message('/dirt/play', ...keyvals);
|
||||
const bundle = new OSC.Bundle([message], ts);
|
||||
bundle.timestamp(ts); // workaround for https://github.com/adzialocha/osc-js/issues/60
|
||||
comm.send(bundle);
|
||||
};
|
||||
return hap.setContext({ ...hap.context, onTrigger });
|
||||
Pattern.prototype.osc = async function () {
|
||||
const osc = await connect();
|
||||
return this.onTrigger((time, hap, currentTime, cps = 1) => {
|
||||
const cycle = hap.wholeOrPart().begin.valueOf();
|
||||
const delta = hap.duration.valueOf();
|
||||
// time should be audio time of onset
|
||||
// currentTime should be current time of audio context (slightly before time)
|
||||
if (startedAt < 0) {
|
||||
startedAt = Date.now() - currentTime * 1000;
|
||||
}
|
||||
const controls = Object.assign({}, { cps, cycle, delta }, hap.value);
|
||||
// make sure n and note are numbers
|
||||
controls.n && (controls.n = parseNumeral(controls.n));
|
||||
controls.note && (controls.note = parseNumeral(controls.note));
|
||||
const keyvals = Object.entries(controls).flat();
|
||||
const ts = Math.floor(startedAt + (time + latency) * 1000);
|
||||
const message = new OSC.Message('/dirt/play', ...keyvals);
|
||||
const bundle = new OSC.Bundle([message], ts);
|
||||
bundle.timestamp(ts); // workaround for https://github.com/adzialocha/osc-js/issues/60
|
||||
osc.send(bundle);
|
||||
});
|
||||
};
|
||||
|
||||
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 Y from "@uiw/react-codemirror";
|
||||
import { Decoration as A, EditorView as K } from "@codemirror/view";
|
||||
import { StateEffect as U, StateField as Q } from "@codemirror/state";
|
||||
import { javascript as Z } from "@codemirror/lang-javascript";
|
||||
import { tags as m } from "@lezer/highlight";
|
||||
import { createTheme as ee } from "@uiw/codemirror-themes";
|
||||
import { useInView as te } from "react-hook-inview";
|
||||
import { evaluate as G } from "@strudel.cycles/eval";
|
||||
import { Tone as M } from "@strudel.cycles/tone";
|
||||
import { TimeSpan as oe, State as re } from "@strudel.cycles/core";
|
||||
import { webaudioOutputTrigger as ne } from "@strudel.cycles/webaudio";
|
||||
import { WebMidi as N, enableWebMidi as se } from "@strudel.cycles/midi";
|
||||
const ae = ee({
|
||||
import n, { useCallback as N, useRef as x, useEffect as R, useState as w, useMemo as I, useLayoutEffect as K } from "react";
|
||||
import Q from "@uiw/react-codemirror";
|
||||
import { Decoration as E, EditorView as O } from "@codemirror/view";
|
||||
import { StateEffect as j, StateField as U } from "@codemirror/state";
|
||||
import { javascript as W } from "@codemirror/lang-javascript";
|
||||
import { tags as r } from "@lezer/highlight";
|
||||
import { createTheme as X } from "@uiw/codemirror-themes";
|
||||
import { useInView as Y } from "react-hook-inview";
|
||||
import { webaudioOutput as Z, getAudioContext as ee } from "@strudel.cycles/webaudio";
|
||||
import { repl as te } from "@strudel.cycles/core";
|
||||
import { transpiler as re } from "@strudel.cycles/transpiler";
|
||||
const oe = X({
|
||||
theme: "dark",
|
||||
settings: {
|
||||
background: "#222",
|
||||
@ -19,450 +17,294 @@ const ae = ee({
|
||||
caret: "#ffcc00",
|
||||
selection: "rgba(128, 203, 196, 0.5)",
|
||||
selectionMatch: "#036dd626",
|
||||
lineHighlight: "#8a91991a",
|
||||
lineHighlight: "#00000050",
|
||||
gutterBackground: "transparent",
|
||||
gutterForeground: "#676e95"
|
||||
gutterForeground: "#8a919966"
|
||||
},
|
||||
styles: [
|
||||
{ tag: m.keyword, color: "#c792ea" },
|
||||
{ tag: m.operator, color: "#89ddff" },
|
||||
{ tag: m.special(m.variableName), color: "#eeffff" },
|
||||
{ tag: m.typeName, color: "#f07178" },
|
||||
{ tag: m.atom, color: "#f78c6c" },
|
||||
{ tag: m.number, color: "#ff5370" },
|
||||
{ tag: m.definition(m.variableName), color: "#82aaff" },
|
||||
{ tag: m.string, color: "#c3e88d" },
|
||||
{ tag: m.special(m.string), color: "#f07178" },
|
||||
{ tag: m.comment, color: "#7d8799" },
|
||||
{ tag: m.variableName, color: "#f07178" },
|
||||
{ tag: m.tagName, color: "#ff5370" },
|
||||
{ tag: m.bracket, color: "#a2a1a4" },
|
||||
{ tag: m.meta, color: "#ffcb6b" },
|
||||
{ tag: m.attributeName, color: "#c792ea" },
|
||||
{ tag: m.propertyName, color: "#c792ea" },
|
||||
{ tag: m.className, color: "#decb6b" },
|
||||
{ tag: m.invalid, color: "#ffffff" }
|
||||
{ tag: r.keyword, color: "#c792ea" },
|
||||
{ tag: r.operator, color: "#89ddff" },
|
||||
{ tag: r.special(r.variableName), color: "#eeffff" },
|
||||
{ tag: r.typeName, color: "#c3e88d" },
|
||||
{ tag: r.atom, color: "#f78c6c" },
|
||||
{ tag: r.number, color: "#c3e88d" },
|
||||
{ tag: r.definition(r.variableName), color: "#82aaff" },
|
||||
{ tag: r.string, color: "#c3e88d" },
|
||||
{ tag: r.special(r.string), color: "#c3e88d" },
|
||||
{ tag: r.comment, color: "#7d8799" },
|
||||
{ tag: r.variableName, color: "#c792ea" },
|
||||
{ tag: r.tagName, color: "#c3e88d" },
|
||||
{ tag: r.bracket, color: "#525154" },
|
||||
{ tag: r.meta, color: "#ffcb6b" },
|
||||
{ tag: r.attributeName, color: "#c792ea" },
|
||||
{ tag: r.propertyName, color: "#c792ea" },
|
||||
{ tag: r.className, color: "#decb6b" },
|
||||
{ tag: r.invalid, color: "#ffffff" }
|
||||
]
|
||||
});
|
||||
const z = U.define(), ce = Q.define({
|
||||
const T = j.define(), ne = U.define({
|
||||
create() {
|
||||
return A.none;
|
||||
return E.none;
|
||||
},
|
||||
update(e, o) {
|
||||
update(e, t) {
|
||||
try {
|
||||
for (let r of o.effects)
|
||||
if (r.is(z))
|
||||
if (r.value) {
|
||||
const n = A.mark({ attributes: { style: "background-color: #FFCA2880" } });
|
||||
e = A.set([n.range(0, o.newDoc.length)]);
|
||||
for (let o of t.effects)
|
||||
if (o.is(T))
|
||||
if (o.value) {
|
||||
const a = E.mark({ attributes: { style: "background-color: #FFCA2880" } });
|
||||
e = E.set([a.range(0, t.newDoc.length)]);
|
||||
} else
|
||||
e = A.set([]);
|
||||
e = E.set([]);
|
||||
return e;
|
||||
} catch (r) {
|
||||
return console.warn("flash error", r), e;
|
||||
} catch (o) {
|
||||
return console.warn("flash error", o), e;
|
||||
}
|
||||
},
|
||||
provide: (e) => K.decorations.from(e)
|
||||
}), ie = (e) => {
|
||||
e.dispatch({ effects: z.of(!0) }), setTimeout(() => {
|
||||
e.dispatch({ effects: z.of(!1) });
|
||||
provide: (e) => O.decorations.from(e)
|
||||
}), ae = (e) => {
|
||||
e.dispatch({ effects: T.of(!0) }), setTimeout(() => {
|
||||
e.dispatch({ effects: T.of(!1) });
|
||||
}, 200);
|
||||
}, O = U.define(), le = Q.define({
|
||||
}, A = j.define(), se = U.define({
|
||||
create() {
|
||||
return A.none;
|
||||
return E.none;
|
||||
},
|
||||
update(e, o) {
|
||||
update(e, t) {
|
||||
try {
|
||||
for (let r of o.effects)
|
||||
if (r.is(O)) {
|
||||
const n = r.value.map(
|
||||
(s) => (s.context.locations || []).map(({ start: i, end: a }) => {
|
||||
const t = s.context.color || "#FFCA28";
|
||||
let l = o.newDoc.line(i.line).from + i.column, c = o.newDoc.line(a.line).from + a.column;
|
||||
const f = o.newDoc.length;
|
||||
return l > f || c > f ? void 0 : A.mark({ attributes: { style: `outline: 1.5px solid ${t};` } }).range(l, c);
|
||||
for (let o of t.effects)
|
||||
if (o.is(A)) {
|
||||
const a = o.value.map(
|
||||
(s) => (s.context.locations || []).map(({ start: m, end: l }) => {
|
||||
const d = s.context.color || "#FFCA28";
|
||||
let c = t.newDoc.line(m.line).from + m.column, i = t.newDoc.line(l.line).from + l.column;
|
||||
const g = t.newDoc.length;
|
||||
return c > g || i > g ? void 0 : E.mark({ attributes: { style: `outline: 1.5px solid ${d};` } }).range(c, i);
|
||||
})
|
||||
).flat().filter(Boolean) || [];
|
||||
e = A.set(n, !0);
|
||||
e = E.set(a, !0);
|
||||
}
|
||||
return e;
|
||||
} catch {
|
||||
return A.set([]);
|
||||
return E.set([]);
|
||||
}
|
||||
},
|
||||
provide: (e) => K.decorations.from(e)
|
||||
}), ue = [Z(), ae, le, ce];
|
||||
function de({ value: e, onChange: o, onViewChanged: r, onSelectionChange: n, options: s, editorDidMount: i }) {
|
||||
const a = T(
|
||||
(c) => {
|
||||
o?.(c);
|
||||
provide: (e) => O.decorations.from(e)
|
||||
}), ce = [W(), oe, se, ne];
|
||||
function ie({ value: e, onChange: t, onViewChanged: o, onSelectionChange: a, options: s, editorDidMount: m }) {
|
||||
const l = N(
|
||||
(i) => {
|
||||
t?.(i);
|
||||
},
|
||||
[t]
|
||||
), d = N(
|
||||
(i) => {
|
||||
o?.(i);
|
||||
},
|
||||
[o]
|
||||
), t = T(
|
||||
(c) => {
|
||||
r?.(c);
|
||||
), c = N(
|
||||
(i) => {
|
||||
i.selectionSet && a && a?.(i.state.selection);
|
||||
},
|
||||
[r]
|
||||
), l = T(
|
||||
(c) => {
|
||||
c.selectionSet && n && n?.(c.state.selection);
|
||||
},
|
||||
[n]
|
||||
[a]
|
||||
);
|
||||
return /* @__PURE__ */ y.createElement(y.Fragment, null, /* @__PURE__ */ y.createElement(Y, {
|
||||
return /* @__PURE__ */ n.createElement(n.Fragment, null, /* @__PURE__ */ n.createElement(Q, {
|
||||
value: e,
|
||||
onChange: a,
|
||||
onCreateEditor: t,
|
||||
onUpdate: l,
|
||||
extensions: ue
|
||||
onChange: l,
|
||||
onCreateEditor: d,
|
||||
onUpdate: c,
|
||||
extensions: ce
|
||||
}));
|
||||
}
|
||||
function fe(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) {
|
||||
function B(...e) {
|
||||
return e.filter(Boolean).join(" ");
|
||||
}
|
||||
function ye({ view: e, pattern: o, active: r, getTime: n }) {
|
||||
const s = I([]), i = I();
|
||||
q(() => {
|
||||
function le({ view: e, pattern: t, active: o, getTime: a }) {
|
||||
const s = x([]), m = x();
|
||||
R(() => {
|
||||
if (e)
|
||||
if (o && r) {
|
||||
let t = function() {
|
||||
if (t && o) {
|
||||
let d = function() {
|
||||
try {
|
||||
const l = n(), f = [Math.max(i.current || l, l - 1 / 10), l + 1 / 60];
|
||||
i.current = l + 1 / 60, s.current = s.current.filter((d) => d.whole.end > l);
|
||||
const p = o.queryArc(...f).filter((d) => d.hasOnset());
|
||||
s.current = s.current.concat(p), e.dispatch({ effects: O.of(s.current) });
|
||||
const c = a(), g = [Math.max(m.current || c, c - 1 / 10, 0), c + 1 / 60];
|
||||
m.current = g[1], s.current = s.current.filter((p) => p.whole.end > c);
|
||||
const v = t.queryArc(...g).filter((p) => p.hasOnset());
|
||||
s.current = s.current.concat(v), e.dispatch({ effects: A.of(s.current) });
|
||||
} catch {
|
||||
e.dispatch({ effects: O.of([]) });
|
||||
e.dispatch({ effects: A.of([]) });
|
||||
}
|
||||
a = requestAnimationFrame(t);
|
||||
}, a = requestAnimationFrame(t);
|
||||
l = requestAnimationFrame(d);
|
||||
}, l = requestAnimationFrame(d);
|
||||
return () => {
|
||||
cancelAnimationFrame(a);
|
||||
cancelAnimationFrame(l);
|
||||
};
|
||||
} else
|
||||
s.current = [], e.dispatch({ effects: O.of([]) });
|
||||
}, [o, r, e]);
|
||||
s.current = [], e.dispatch({ effects: A.of([]) });
|
||||
}, [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 = {
|
||||
container: be,
|
||||
header: we,
|
||||
buttons: ve,
|
||||
button: Ee,
|
||||
buttonDisabled: ke,
|
||||
error: Ce,
|
||||
body: Me
|
||||
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: de,
|
||||
header: ue,
|
||||
buttons: fe,
|
||||
button: me,
|
||||
buttonDisabled: ge,
|
||||
error: pe,
|
||||
body: he
|
||||
};
|
||||
function W({ type: e }) {
|
||||
return /* @__PURE__ */ y.createElement("svg", {
|
||||
function q({ type: e }) {
|
||||
return /* @__PURE__ */ n.createElement("svg", {
|
||||
xmlns: "http://www.w3.org/2000/svg",
|
||||
className: "sc-h-5 sc-w-5",
|
||||
viewBox: "0 0 20 20",
|
||||
fill: "currentColor"
|
||||
}, {
|
||||
refresh: /* @__PURE__ */ y.createElement("path", {
|
||||
refresh: /* @__PURE__ */ n.createElement("path", {
|
||||
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",
|
||||
clipRule: "evenodd"
|
||||
}),
|
||||
play: /* @__PURE__ */ y.createElement("path", {
|
||||
play: /* @__PURE__ */ n.createElement("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"
|
||||
}),
|
||||
pause: /* @__PURE__ */ y.createElement("path", {
|
||||
pause: /* @__PURE__ */ n.createElement("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"
|
||||
})
|
||||
}[e]);
|
||||
}
|
||||
function Be({ tune: e, hideOutsideView: o = !1, init: r, onEvent: n, enableKeyboard: s }) {
|
||||
const { code: i, setCode: a, pattern: t, activeCode: l, activateCode: c, evaluateOnly: f, error: p, cycle: d, dirty: k, togglePlay: g, stop: C } = pe({
|
||||
tune: e,
|
||||
autolink: !1,
|
||||
onEvent: n
|
||||
});
|
||||
q(() => {
|
||||
r && f();
|
||||
}, [e, r]);
|
||||
const [v, h] = w(), [D, _] = te({
|
||||
threshold: 0.01
|
||||
}), E = I(), L = R(() => ((_ || !o) && (E.current = !0), _ || E.current), [_, o]);
|
||||
return ye({
|
||||
view: v,
|
||||
pattern: t,
|
||||
active: d.started && !l?.includes("strudel disable-highlighting"),
|
||||
getTime: () => M.getTransport().seconds
|
||||
}), j(() => {
|
||||
if (s) {
|
||||
const H = async (x) => {
|
||||
(x.ctrlKey || x.altKey) && (x.code === "Enter" ? (x.preventDefault(), ie(v), await c()) : x.code === "Period" && (d.stop(), x.preventDefault()));
|
||||
};
|
||||
return window.addEventListener("keydown", H, !0), () => window.removeEventListener("keydown", H, !0);
|
||||
function ve({
|
||||
defaultOutput: e,
|
||||
interval: t,
|
||||
getTime: o,
|
||||
evalOnMount: a = !1,
|
||||
initialCode: s = "",
|
||||
autolink: m = !1,
|
||||
beforeEval: l,
|
||||
afterEval: d,
|
||||
onEvalError: c,
|
||||
onToggle: i
|
||||
}) {
|
||||
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(
|
||||
() => te({
|
||||
interval: t,
|
||||
defaultOutput: e,
|
||||
onSchedulerError: v,
|
||||
onEvalError: (f) => {
|
||||
D(f), c?.(f);
|
||||
},
|
||||
getTime: o,
|
||||
transpiler: re,
|
||||
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
|
||||
}, /* @__PURE__ */ y.createElement("div", {
|
||||
className: F.header
|
||||
}, /* @__PURE__ */ y.createElement("div", {
|
||||
className: F.buttons
|
||||
}, /* @__PURE__ */ y.createElement("button", {
|
||||
className: $(F.button, d.started ? "sc-animate-pulse" : ""),
|
||||
onClick: () => g()
|
||||
}, /* @__PURE__ */ y.createElement(W, {
|
||||
type: d.started ? "pause" : "play"
|
||||
})), /* @__PURE__ */ y.createElement("button", {
|
||||
className: $(k ? F.button : F.buttonDisabled),
|
||||
onClick: () => c()
|
||||
}, /* @__PURE__ */ y.createElement(W, {
|
||||
};
|
||||
}
|
||||
const be = () => ee().currentTime;
|
||||
function Pe({ tune: e, hideOutsideView: t = !1, init: o, enableKeyboard: a }) {
|
||||
const {
|
||||
code: s,
|
||||
setCode: m,
|
||||
evaluate: l,
|
||||
activateCode: d,
|
||||
error: c,
|
||||
isDirty: i,
|
||||
activeCode: g,
|
||||
pattern: v,
|
||||
started: p,
|
||||
scheduler: D,
|
||||
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"
|
||||
}))), p && /* @__PURE__ */ y.createElement("div", {
|
||||
className: F.error
|
||||
}, p.message)), /* @__PURE__ */ y.createElement("div", {
|
||||
className: F.body
|
||||
}, L && /* @__PURE__ */ y.createElement(de, {
|
||||
value: i,
|
||||
onChange: a,
|
||||
onViewChanged: h
|
||||
}))), c && /* @__PURE__ */ n.createElement("div", {
|
||||
className: b.error
|
||||
}, c.message)), /* @__PURE__ */ n.createElement("div", {
|
||||
className: b.body
|
||||
}, H && /* @__PURE__ */ n.createElement(ie, {
|
||||
value: s,
|
||||
onChange: m,
|
||||
onViewChanged: P
|
||||
})));
|
||||
}
|
||||
function Te(e, o, r = 0.05, n = 0.1, s = 0.1) {
|
||||
let i = 0, a = 0, t = 10 ** 4, l = 0.01;
|
||||
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) };
|
||||
function ze(e) {
|
||||
return R(() => (window.addEventListener("message", e), () => window.removeEventListener("message", e)), [e]), N((t) => window.postMessage(t, "*"), []);
|
||||
}
|
||||
const He = (e) => K(() => (window.addEventListener("keydown", e, !0), () => window.removeEventListener("keydown", e, !0)), [e]);
|
||||
export {
|
||||
de as CodeMirror,
|
||||
Be as MiniRepl,
|
||||
$ as cx,
|
||||
ie as flash,
|
||||
fe as useCycle,
|
||||
ye as useHighlighting,
|
||||
$e as useKeydown,
|
||||
ge as usePostMessage,
|
||||
pe as useRepl,
|
||||
Ve as useStrudel,
|
||||
We as useWebMidi
|
||||
ie as CodeMirror,
|
||||
Pe as MiniRepl,
|
||||
B as cx,
|
||||
ae as flash,
|
||||
le as useHighlighting,
|
||||
He as useKeydown,
|
||||
ze as usePostMessage,
|
||||
ve as useStrudel
|
||||
};
|
||||
|
||||
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,
|
||||
pattern,
|
||||
active: !activeCode?.includes('strudel disable-highlighting'),
|
||||
getTime: () => scheduler.phase,
|
||||
getTime: () => scheduler.getPhase(),
|
||||
});
|
||||
|
||||
const error = evalError || schedulerError;
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"watch": "vite build --watch",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"repository": {
|
||||
@ -39,7 +40,8 @@
|
||||
"dependencies": {
|
||||
"@codemirror/lang-javascript": "^6.1.1",
|
||||
"@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",
|
||||
"@uiw/codemirror-themes": "^4.12.4",
|
||||
"@uiw/react-codemirror": "^4.12.4",
|
||||
|
||||
@ -6,7 +6,7 @@ import { controls, evalScope } from '@strudel.cycles/core';
|
||||
evalScope(
|
||||
controls,
|
||||
import('@strudel.cycles/core'),
|
||||
import('@strudel.cycles/tone'),
|
||||
// import('@strudel.cycles/tone'),
|
||||
import('@strudel.cycles/tonal'),
|
||||
import('@strudel.cycles/mini'),
|
||||
import('@strudel.cycles/midi'),
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import React, { useState, useMemo, useRef, useEffect, useLayoutEffect } from 'react';
|
||||
import { useInView } from 'react-hook-inview';
|
||||
import useRepl from '../hooks/useRepl.mjs';
|
||||
import cx from '../cx';
|
||||
import useHighlighting from '../hooks/useHighlighting.mjs';
|
||||
import CodeMirror6, { flash } from './CodeMirror6';
|
||||
@ -8,18 +7,33 @@ import 'tailwindcss/tailwind.css';
|
||||
import './style.css';
|
||||
import styles from './MiniRepl.module.css';
|
||||
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 { code, setCode, pattern, activeCode, activateCode, evaluateOnly, error, cycle, dirty, togglePlay, stop } =
|
||||
useRepl({
|
||||
tune,
|
||||
autolink: false,
|
||||
onEvent,
|
||||
});
|
||||
useEffect(() => {
|
||||
init && evaluateOnly();
|
||||
}, [tune, init]);
|
||||
const getTime = () => getAudioContext().currentTime;
|
||||
|
||||
export function MiniRepl({ tune, hideOutsideView = false, init, enableKeyboard }) {
|
||||
const {
|
||||
code,
|
||||
setCode,
|
||||
evaluate,
|
||||
activateCode,
|
||||
error,
|
||||
isDirty,
|
||||
activeCode,
|
||||
pattern,
|
||||
started,
|
||||
scheduler,
|
||||
togglePlay,
|
||||
stop,
|
||||
} = useStrudel({
|
||||
initialCode: tune,
|
||||
defaultOutput: webaudioOutput,
|
||||
getTime,
|
||||
});
|
||||
/* useEffect(() => {
|
||||
init && activateCode();
|
||||
}, [init, activateCode]); */
|
||||
const [view, setView] = useState();
|
||||
const [ref, isVisible] = useInView({
|
||||
threshold: 0.01,
|
||||
@ -34,8 +48,8 @@ export function MiniRepl({ tune, hideOutsideView = false, init, onEvent, enableK
|
||||
useHighlighting({
|
||||
view,
|
||||
pattern,
|
||||
active: cycle.started && !activeCode?.includes('strudel disable-highlighting'),
|
||||
getTime: () => Tone.getTransport().seconds,
|
||||
active: started && !activeCode?.includes('strudel disable-highlighting'),
|
||||
getTime: () => scheduler.getPhase(),
|
||||
});
|
||||
|
||||
// set active pattern on ctrl+enter
|
||||
@ -48,7 +62,7 @@ export function MiniRepl({ tune, hideOutsideView = false, init, onEvent, enableK
|
||||
flash(view);
|
||||
await activateCode();
|
||||
} else if (e.code === 'Period') {
|
||||
cycle.stop();
|
||||
stop();
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
@ -56,16 +70,16 @@ export function MiniRepl({ tune, hideOutsideView = false, init, onEvent, enableK
|
||||
window.addEventListener('keydown', handleKeyPress, true);
|
||||
return () => window.removeEventListener('keydown', handleKeyPress, true);
|
||||
}
|
||||
}, [enableKeyboard, pattern, code, activateCode, cycle, view]);
|
||||
}, [enableKeyboard, pattern, code, evaluate, stop, view]);
|
||||
|
||||
return (
|
||||
<div className={styles.container} ref={ref}>
|
||||
<div className={styles.header}>
|
||||
<div className={styles.buttons}>
|
||||
<button className={cx(styles.button, cycle.started ? 'sc-animate-pulse' : '')} onClick={() => togglePlay()}>
|
||||
<Icon type={cycle.started ? 'pause' : 'play'} />
|
||||
<button className={cx(styles.button, started ? 'sc-animate-pulse' : '')} onClick={() => togglePlay()}>
|
||||
<Icon type={started ? 'pause' : 'play'} />
|
||||
</button>
|
||||
<button className={cx(dirty ? styles.button : styles.buttonDisabled)} onClick={() => activateCode()}>
|
||||
<button className={cx(isDirty ? styles.button : styles.buttonDisabled)} onClick={() => activateCode()}>
|
||||
<Icon type="refresh" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
background-color: transparent !important;
|
||||
height: 100%;
|
||||
z-index: 11;
|
||||
font-size: 16px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.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();
|
||||
// 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
|
||||
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];
|
||||
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
|
||||
const haps = pattern.queryArc(...span).filter((hap) => hap.hasOnset());
|
||||
highlights.current = highlights.current.concat(haps); // add potential new onsets
|
||||
view.dispatch({ effects: setHighlights.of(highlights.current) }); // highlight all still active + new active haps
|
||||
} catch (err) {
|
||||
// console.log('error in updateHighlights', err);
|
||||
view.dispatch({ effects: setHighlights.of([]) });
|
||||
}
|
||||
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 { repl } from '@strudel.cycles/core/repl.mjs';
|
||||
import { repl } from '@strudel.cycles/core';
|
||||
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
|
||||
const [schedulerError, setSchedulerError] = useState();
|
||||
const [evalError, setEvalError] = useState();
|
||||
const [code, setCode] = useState(initialCode);
|
||||
const [activeCode, setActiveCode] = useState(code);
|
||||
const [pattern, setPattern] = useState();
|
||||
const [started, setStarted] = useState(false);
|
||||
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({
|
||||
interval,
|
||||
defaultOutput,
|
||||
onSchedulerError: setSchedulerError,
|
||||
onEvalError: setEvalError,
|
||||
onEvalError: (err) => {
|
||||
setEvalError(err);
|
||||
onEvalError?.(err);
|
||||
},
|
||||
getTime,
|
||||
transpiler,
|
||||
onEval: ({ pattern: _pattern, code }) => {
|
||||
beforeEval: ({ code }) => {
|
||||
setCode(code);
|
||||
beforeEval?.();
|
||||
},
|
||||
afterEval: ({ pattern: _pattern, code }) => {
|
||||
setActiveCode(code);
|
||||
setPattern(_pattern);
|
||||
setEvalError();
|
||||
setSchedulerError();
|
||||
if (autolink) {
|
||||
window.location.hash = '#' + encodeURIComponent(btoa(code));
|
||||
}
|
||||
afterEval?.();
|
||||
},
|
||||
onToggle: (v) => {
|
||||
setStarted(v);
|
||||
onToggle?.(v);
|
||||
},
|
||||
onEvalError: setEvalError,
|
||||
}),
|
||||
[defaultOutput, interval, getTime],
|
||||
);
|
||||
const evaluate = useCallback(() => _evaluate(code), [_evaluate, code]);
|
||||
const activateCode = useCallback(async (autostart = true) => evaluate(code, autostart), [evaluate, code]);
|
||||
|
||||
const inited = useRef();
|
||||
useEffect(() => {
|
||||
if (!inited.current && evalOnMount && code) {
|
||||
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;
|
||||
|
||||
@ -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 * from './components/MiniRepl';
|
||||
export { default as useCycle } from './hooks/useCycle';
|
||||
export { default as useHighlighting } from './hooks/useHighlighting';
|
||||
export { default as usePostMessage } from './hooks/usePostMessage';
|
||||
export { default as useRepl } from './hooks/useRepl';
|
||||
export { default as useStrudel } from './hooks/useStrudel';
|
||||
export { default as useKeydown } from './hooks/useKeydown';
|
||||
export { default as cx } from './cx';
|
||||
export { useWebMidi } from './hooks/useWebMidi';
|
||||
|
||||
@ -8,28 +8,36 @@ export default createTheme({
|
||||
caret: '#ffcc00',
|
||||
selection: 'rgba(128, 203, 196, 0.5)',
|
||||
selectionMatch: '#036dd626',
|
||||
lineHighlight: '#8a91991a',
|
||||
// lineHighlight: '#8a91991a', // original
|
||||
lineHighlight: '#00000050',
|
||||
gutterBackground: 'transparent',
|
||||
// gutterForeground: '#8a919966',
|
||||
gutterForeground: '#676e95',
|
||||
gutterForeground: '#8a919966',
|
||||
},
|
||||
styles: [
|
||||
{ tag: t.keyword, color: '#c792ea' },
|
||||
{ tag: t.operator, color: '#89ddff' },
|
||||
{ tag: t.special(t.variableName), color: '#eeffff' },
|
||||
{ tag: t.typeName, color: '#f07178' },
|
||||
// { tag: t.typeName, color: '#f07178' }, // original
|
||||
{ tag: t.typeName, color: '#c3e88d' },
|
||||
{ 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.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.variableName, color: '#f07178' },
|
||||
{ tag: t.tagName, color: '#ff5370' },
|
||||
{ tag: t.bracket, color: '#a2a1a4' },
|
||||
// { tag: t.variableName, color: '#f07178' }, // original
|
||||
{ tag: t.variableName, color: '#c792ea' },
|
||||
// { tag: t.tagName, color: '#ff5370' }, // original
|
||||
{ tag: t.tagName, color: '#c3e88d' },
|
||||
{ tag: t.bracket, color: '#525154' },
|
||||
// { tag: t.bracket, color: '#a2a1a4' }, // original
|
||||
{ tag: t.meta, color: '#ffcb6b' },
|
||||
{ tag: t.attributeName, color: '#c792ea' },
|
||||
{ tag: t.propertyName, color: '#c792ea' },
|
||||
|
||||
{ tag: t.className, color: '#decb6b' },
|
||||
{ tag: t.invalid, color: '#ffffff' },
|
||||
],
|
||||
|
||||
@ -24,8 +24,9 @@ export default defineConfig({
|
||||
// TODO: find out which of below names are obsolete now
|
||||
'@strudel.cycles/tone',
|
||||
'@strudel.cycles/eval',
|
||||
'@strudel.cycles/transpiler',
|
||||
'acorn',
|
||||
'@strudel.cycles/core',
|
||||
'@strudel.cycles/core/util.mjs',
|
||||
'@strudel.cycles/mini',
|
||||
'@strudel.cycles/tonal',
|
||||
'@strudel.cycles/midi',
|
||||
|
||||
@ -9,7 +9,7 @@ import { Pattern, isPattern } from '@strudel.cycles/core';
|
||||
var serialWriter;
|
||||
var choosing = false;
|
||||
|
||||
export async function getWriter(br=38400) {
|
||||
export async function getWriter(br = 38400) {
|
||||
if (choosing) {
|
||||
return;
|
||||
}
|
||||
@ -24,11 +24,10 @@ export async function getWriter(br=38400) {
|
||||
const writableStreamClosed = textEncoder.readable.pipeTo(port.writable);
|
||||
const writer = textEncoder.writable.getWriter();
|
||||
serialWriter = function (message) {
|
||||
writer.write(message)
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw('Webserial is not available in this browser.')
|
||||
writer.write(message);
|
||||
};
|
||||
} else {
|
||||
throw 'Webserial is not available in this browser.';
|
||||
}
|
||||
}
|
||||
|
||||
@ -40,7 +39,7 @@ Pattern.prototype.serial = function (...args) {
|
||||
getWriter(...args);
|
||||
}
|
||||
const onTrigger = (time, hap, currentTime) => {
|
||||
var message = "";
|
||||
var message = '';
|
||||
if (typeof hap.value === 'object') {
|
||||
if ('action' in hap.value) {
|
||||
message += hap.value['action'] + '(';
|
||||
@ -51,26 +50,23 @@ Pattern.prototype.serial = function (...args) {
|
||||
}
|
||||
if (first) {
|
||||
first = false;
|
||||
} else {
|
||||
message += ',';
|
||||
}
|
||||
else {
|
||||
message +=',';
|
||||
}
|
||||
message += `${key}:${val}`
|
||||
message += `${key}:${val}`;
|
||||
}
|
||||
message += ')';
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
for (const [key, val] of Object.entries(hap.value)) {
|
||||
message += `${key}:${val};`
|
||||
message += `${key}:${val};`;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
message = hap.value;
|
||||
}
|
||||
const offset = (time - currentTime + latency) * 1000;
|
||||
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 './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
|
||||
Pattern.prototype.tone = function (instrument) {
|
||||
return this._withHap((hap) => {
|
||||
const onTrigger = (time, hap) => {
|
||||
let note;
|
||||
let velocity = hap.context?.velocity ?? 0.75;
|
||||
if (instrument instanceof PluckSynth) {
|
||||
note = getPlayableNoteValue(hap);
|
||||
instrument.triggerAttack(note, time);
|
||||
} else if (instrument instanceof NoiseSynth) {
|
||||
instrument.triggerAttackRelease(hap.duration.valueOf(), time); // noise has no value
|
||||
} else if (instrument instanceof Sampler) {
|
||||
note = getPlayableNoteValue(hap);
|
||||
instrument.triggerAttackRelease(note, hap.duration.valueOf(), time, velocity);
|
||||
} else if (instrument instanceof Players) {
|
||||
if (!instrument.has(hap.value)) {
|
||||
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);
|
||||
return this.onTrigger((time, hap) => {
|
||||
let note;
|
||||
let velocity = hap.context?.velocity ?? 0.75;
|
||||
if (instrument instanceof PluckSynth) {
|
||||
note = getPlayableNoteValue(hap);
|
||||
instrument.triggerAttack(note, time);
|
||||
} else if (instrument instanceof NoiseSynth) {
|
||||
instrument.triggerAttackRelease(hap.duration.valueOf(), time); // noise has no value
|
||||
} else if (instrument instanceof Sampler) {
|
||||
note = getPlayableNoteValue(hap);
|
||||
instrument.triggerAttackRelease(note, hap.duration.valueOf(), time, velocity);
|
||||
} else if (instrument instanceof Players) {
|
||||
if (!instrument.has(hap.value)) {
|
||||
throw new Error(`name "${hap.value}" not defined for players`);
|
||||
}
|
||||
};
|
||||
return hap.setContext({ ...hap.context, instrument, onTrigger });
|
||||
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);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@ -1,13 +1,87 @@
|
||||
import { logger } from '@strudel.cycles/core';
|
||||
|
||||
const bufferCache = {}; // string: Promise<ArrayBuffer>
|
||||
const loadCache = {}; // string: Promise<ArrayBuffer>
|
||||
|
||||
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]) {
|
||||
logger(`[sampler] load ${label}..`, 'load-sample', { url });
|
||||
const timestamp = Date.now();
|
||||
loadCache[url] = fetch(url)
|
||||
.then((res) => res.arrayBuffer())
|
||||
.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);
|
||||
bufferCache[url] = decoded;
|
||||
return decoded;
|
||||
@ -29,66 +103,7 @@ export const getLoadedBuffer = (url) => {
|
||||
return bufferCache[url];
|
||||
};
|
||||
|
||||
/* export const playBuffer = (buffer, time = ac.currentTime, destination = ac.destination) => {
|
||||
const src = ac.createBufferSource();
|
||||
src.buffer = buffer;
|
||||
src.connect(destination);
|
||||
src.start(time);
|
||||
};
|
||||
|
||||
export const playSample = async (url) => playBuffer(await loadBuffer(url)); */
|
||||
|
||||
// https://estuary.mcmaster.ca/samples/resources.json
|
||||
// Array<{ "url":string, "bank": string, "n": number}>
|
||||
// ritchse/tidal-drum-machines/tree/main/machines/AkaiLinn
|
||||
const githubCache = {};
|
||||
let sampleCache = { current: undefined };
|
||||
export const loadGithubSamples = async (path, nameFn) => {
|
||||
const storageKey = 'loadGithubSamples ' + path;
|
||||
const stored = localStorage.getItem(storageKey);
|
||||
if (stored) {
|
||||
console.log('[sampler]: loaded sample list from localstorage', path);
|
||||
githubCache[path] = JSON.parse(stored);
|
||||
}
|
||||
if (githubCache[path]) {
|
||||
sampleCache.current = githubCache[path];
|
||||
return githubCache[path];
|
||||
}
|
||||
console.log('[sampler]: fetching sample list from github', path);
|
||||
try {
|
||||
const [user, repo, ...folders] = path.split('/');
|
||||
const baseUrl = `https://api.github.com/repos/${user}/${repo}/contents`;
|
||||
const banks = await fetch(`${baseUrl}/${folders.join('/')}`).then((res) => res.json());
|
||||
// fetch each subfolder
|
||||
githubCache[path] = (
|
||||
await Promise.all(
|
||||
banks.map(async ({ name, path }) => ({
|
||||
name,
|
||||
content: await fetch(`${baseUrl}/${path}`)
|
||||
.then((res) => res.json())
|
||||
.catch((err) => {
|
||||
console.error('could not load path', err);
|
||||
}),
|
||||
})),
|
||||
)
|
||||
)
|
||||
.filter(({ content }) => !!content)
|
||||
.reduce(
|
||||
(acc, { name, content }) => ({
|
||||
...acc,
|
||||
[nameFn?.(name) || name]: content.map(({ download_url }) => download_url),
|
||||
}),
|
||||
{},
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('[sampler]: failed to fetch sample list from github', err);
|
||||
return;
|
||||
}
|
||||
sampleCache.current = githubCache[path];
|
||||
localStorage.setItem(storageKey, JSON.stringify(sampleCache.current));
|
||||
console.log('[sampler]: loaded samples:', sampleCache.current);
|
||||
return githubCache[path];
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 * 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 './reverb.mjs';
|
||||
import { loadBuffer, reverseBuffer } from './sampler.mjs';
|
||||
import { getSampleBufferSource } from './sampler.mjs';
|
||||
const { Pattern } = strudel;
|
||||
import './vowel.mjs';
|
||||
import workletsUrl from './worklets.mjs?url';
|
||||
@ -98,56 +98,6 @@ const getSoundfontKey = (s) => {
|
||||
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) => {
|
||||
if (!s.includes(':')) {
|
||||
return [s, n];
|
||||
@ -176,14 +126,25 @@ function getWorklet(ac, processor, params) {
|
||||
return node;
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
try {
|
||||
loadWorklets();
|
||||
} catch (err) {
|
||||
console.warn('could not load AudioWorklet effects coarse, crush and shape', err);
|
||||
// this function should be called on first user interaction (to avoid console warning)
|
||||
export function initAudio() {
|
||||
if (typeof window !== 'undefined') {
|
||||
try {
|
||||
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) {
|
||||
const node = getAudioContext().createGain();
|
||||
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 (hap, deadline, hapDuration) => {
|
||||
try {
|
||||
const ac = getAudioContext();
|
||||
/* if (isNote(hap.value)) {
|
||||
const ac = getAudioContext();
|
||||
/* if (isNote(hap.value)) {
|
||||
// supports primitive hap values that look like notes
|
||||
hap.value = { note: hap.value };
|
||||
} */
|
||||
if (typeof hap.value !== 'object') {
|
||||
throw new Error(
|
||||
`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);
|
||||
if (typeof hap.value !== 'object') {
|
||||
throw new Error(
|
||||
`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;
|
||||
|
||||
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);
|
||||
|
||||
@ -62,37 +62,34 @@ export function loadWebDirt(config) {
|
||||
*/
|
||||
Pattern.prototype.webdirt = function () {
|
||||
// create a WebDirt object and initialize Web Audio context
|
||||
return this._withHap((hap) => {
|
||||
const onTrigger = async (time, e, currentTime) => {
|
||||
if (!webDirt) {
|
||||
throw new Error('WebDirt not initialized!');
|
||||
}
|
||||
const deadline = time - currentTime;
|
||||
const { s, n = 0, ...rest } = e.value || {};
|
||||
if (!s) {
|
||||
console.warn('Pattern.webdirt: no "s" was set!');
|
||||
}
|
||||
const samples = getLoadedSamples();
|
||||
if (!samples?.[s]) {
|
||||
// try default samples
|
||||
webDirt.playSample({ s, n, ...rest }, deadline);
|
||||
return;
|
||||
}
|
||||
if (!samples?.[s]) {
|
||||
console.warn(`Pattern.webdirt: sample "${s}" not found in loaded samples`, samples);
|
||||
return this.onTrigger(async (time, e, currentTime) => {
|
||||
if (!webDirt) {
|
||||
throw new Error('WebDirt not initialized!');
|
||||
}
|
||||
const deadline = time - currentTime;
|
||||
const { s, n = 0, ...rest } = e.value || {};
|
||||
if (!s) {
|
||||
console.warn('Pattern.webdirt: no "s" was set!');
|
||||
}
|
||||
const samples = getLoadedSamples();
|
||||
if (!samples?.[s]) {
|
||||
// try default samples
|
||||
webDirt.playSample({ s, n, ...rest }, deadline);
|
||||
return;
|
||||
}
|
||||
if (!samples?.[s]) {
|
||||
console.warn(`Pattern.webdirt: sample "${s}" not found in loaded samples`, samples);
|
||||
} else {
|
||||
const bank = samples[s];
|
||||
const sampleUrl = bank[n % bank.length];
|
||||
const buffer = getLoadedBuffer(sampleUrl);
|
||||
if (!buffer) {
|
||||
console.log(`Pattern.webdirt: load ${s}:${n} from ${sampleUrl}`);
|
||||
loadBuffer(sampleUrl, webDirt.ac);
|
||||
} else {
|
||||
const bank = samples[s];
|
||||
const sampleUrl = bank[n % bank.length];
|
||||
const buffer = getLoadedBuffer(sampleUrl);
|
||||
if (!buffer) {
|
||||
console.log(`Pattern.webdirt: load ${s}:${n} from ${sampleUrl}`);
|
||||
loadBuffer(sampleUrl, webDirt.ac);
|
||||
} else {
|
||||
const msg = { buffer: { buffer }, ...rest };
|
||||
webDirt.playSample(msg, deadline);
|
||||
}
|
||||
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",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@heroicons/react": "^2.0.13",
|
||||
"@supabase/supabase-js": "^1.35.3",
|
||||
"nanoid": "^4.0.0",
|
||||
"react": "^17.0.2",
|
||||
@ -449,6 +450,14 @@
|
||||
"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": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz",
|
||||
@ -2972,6 +2981,12 @@
|
||||
"dev": 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": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz",
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"predev": "cd ${PWD}/../tutorial/ && npm run render",
|
||||
"dev": "vite --host",
|
||||
"start": "vite",
|
||||
"build": "vite build",
|
||||
@ -16,6 +17,7 @@
|
||||
"dbdump": "node src/test/dbdump.js > src/test/dbdump.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@heroicons/react": "^2.0.13",
|
||||
"@supabase/supabase-js": "^1.35.3",
|
||||
"nanoid": "^4.0.0",
|
||||
"react": "^17.0.2",
|
||||
|
||||
@ -18,3 +18,18 @@ body {
|
||||
background: black;
|
||||
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/>.
|
||||
*/
|
||||
|
||||
import { evaluate } from '@strudel.cycles/eval';
|
||||
import { CodeMirror, cx, flash, useHighlighting, useRepl, useWebMidi } from '@strudel.cycles/react';
|
||||
import { cleanupDraw, cleanupUi, Tone } from '@strudel.cycles/tone';
|
||||
import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||
import './App.css';
|
||||
import logo from './logo.svg';
|
||||
import * as tunes from './tunes.mjs';
|
||||
import { prebake } from './prebake.mjs';
|
||||
import * as WebDirt from 'WebDirt';
|
||||
import { resetLoadedSamples, getAudioContext } from '@strudel.cycles/webaudio';
|
||||
import { controls, evalScope } from '@strudel.cycles/core';
|
||||
import { cleanupDraw, cleanupUi, controls, evalScope, logger } from '@strudel.cycles/core';
|
||||
import { CodeMirror, cx, flash, useHighlighting, useStrudel } from '@strudel.cycles/react';
|
||||
import {
|
||||
getAudioContext,
|
||||
getLoadedSamples,
|
||||
initAudioOnFirstClick,
|
||||
resetLoadedSamples,
|
||||
webaudioOutput,
|
||||
} from '@strudel.cycles/webaudio';
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
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
|
||||
const supabase = createClient(
|
||||
@ -24,12 +31,9 @@ const supabase = createClient(
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InBpZHhkc3hwaGxoempuem1pZnRoIiwicm9sZSI6ImFub24iLCJpYXQiOjE2NTYyMzA1NTYsImV4cCI6MTk3MTgwNjU1Nn0.bqlw7802fsWRnqU5BLYtmXk_k-D1VFmbkHMywWc15NM',
|
||||
);
|
||||
|
||||
evalScope(
|
||||
Tone,
|
||||
controls, // sadly, this cannot be exported from core direclty
|
||||
{ WebDirt },
|
||||
const modules = [
|
||||
import('@strudel.cycles/core'),
|
||||
import('@strudel.cycles/tone'),
|
||||
// import('@strudel.cycles/tone'),
|
||||
import('@strudel.cycles/tonal'),
|
||||
import('@strudel.cycles/mini'),
|
||||
import('@strudel.cycles/midi'),
|
||||
@ -38,9 +42,24 @@ evalScope(
|
||||
import('@strudel.cycles/osc'),
|
||||
import('@strudel.cycles/serial'),
|
||||
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() {
|
||||
// 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];
|
||||
// looking like https://strudel.tidalcycles.org/?J01s5i1J0200 (fixed hash length)
|
||||
if (codeParam) {
|
||||
console.log('decode hash from url');
|
||||
// looking like https://strudel.tidalcycles.org/#ImMzIGUzIg%3D%3D (hash length depends on code length)
|
||||
return atob(decodeURIComponent(codeParam || ''));
|
||||
} else if (hash) {
|
||||
@ -74,269 +92,202 @@ async function initCode() {
|
||||
}
|
||||
|
||||
function getRandomTune() {
|
||||
const allTunes = Object.values(tunes);
|
||||
const allTunes = Object.entries(tunes);
|
||||
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 isEmbedded = window.location !== window.parent.location;
|
||||
const { code: randomTune, name } = getRandomTune();
|
||||
|
||||
export const AppContext = createContext();
|
||||
|
||||
function App() {
|
||||
// const [editor, setEditor] = useState();
|
||||
const [view, setView] = useState();
|
||||
const [view, setView] = useState(); // codemirror view
|
||||
const [lastShared, setLastShared] = useState();
|
||||
const {
|
||||
setCode,
|
||||
setPattern,
|
||||
error,
|
||||
code,
|
||||
cycle,
|
||||
dirty,
|
||||
log,
|
||||
togglePlay,
|
||||
activeCode,
|
||||
setActiveCode,
|
||||
activateCode,
|
||||
pattern,
|
||||
pushLog,
|
||||
pending,
|
||||
hideHeader,
|
||||
hideConsole,
|
||||
} = useRepl({
|
||||
tune: '// LOADING...',
|
||||
});
|
||||
const [activeFooter, setActiveFooter] = useState('');
|
||||
const [isZen, setIsZen] = useState(false);
|
||||
const [pending, setPending] = useState(false);
|
||||
|
||||
const { code, setCode, scheduler, evaluate, activateCode, isDirty, activeCode, pattern, started, stop, error } =
|
||||
useStrudel({
|
||||
initialCode: '// LOADING',
|
||||
defaultOutput: webaudioOutput,
|
||||
getTime,
|
||||
autolink: true,
|
||||
beforeEval: () => {
|
||||
cleanupUi();
|
||||
cleanupDraw();
|
||||
setPending(true);
|
||||
},
|
||||
afterEval: () => {
|
||||
setPending(false);
|
||||
},
|
||||
onToggle: (play) => !play && cleanupDraw(false),
|
||||
});
|
||||
|
||||
// init code
|
||||
useEffect(() => {
|
||||
initCode().then((decoded) => setCode(decoded || randomTune));
|
||||
}, []);
|
||||
const logBox = useRef();
|
||||
// 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();
|
||||
}
|
||||
initCode().then((decoded) => {
|
||||
if (!decoded) {
|
||||
setActiveFooter('intro'); // TODO: get rid
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', handleKeyPress, true);
|
||||
return () => window.removeEventListener('keydown', handleKeyPress, true);
|
||||
}, [pattern, code, activateCode, cycle, view]);
|
||||
logger(
|
||||
`Welcome to Strudel! ${
|
||||
decoded ? `I have loaded the code from the URL.` : `A random code snippet named "${name}" has been loaded!`
|
||||
} 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({
|
||||
view,
|
||||
pattern,
|
||||
active: cycle.started && !activeCode?.includes('strudel disable-highlighting'),
|
||||
getTime: () => Tone.getTransport().seconds,
|
||||
active: started && !activeCode?.includes('strudel disable-highlighting'),
|
||||
getTime: () => scheduler.getPhase(),
|
||||
});
|
||||
|
||||
useWebMidi({
|
||||
ready: useCallback(
|
||||
({ outputs }) => {
|
||||
pushLog(`WebMidi ready! Just add .midi(${outputs.map((o) => `'${o.name}'`).join(' | ')}) to the pattern. `);
|
||||
},
|
||||
[pushLog],
|
||||
),
|
||||
connected: useCallback(
|
||||
({ outputs }) => {
|
||||
pushLog(`Midi device connected! Available: ${outputs.map((o) => `'${o.name}'`).join(', ')}`);
|
||||
},
|
||||
[pushLog],
|
||||
),
|
||||
disconnected: useCallback(
|
||||
({ outputs }) => {
|
||||
pushLog(`Midi device disconnected! Available: ${outputs.map((o) => `'${o.name}'`).join(', ')}`);
|
||||
},
|
||||
[pushLog],
|
||||
),
|
||||
});
|
||||
//
|
||||
// UI Actions
|
||||
//
|
||||
|
||||
const handleChangeCode = useCallback(
|
||||
(c) => {
|
||||
setCode(c);
|
||||
started && logger('[edit] code changed. hit ctrl+enter to update');
|
||||
},
|
||||
[started],
|
||||
);
|
||||
const handleSelectionChange = useCallback((selection) => {
|
||||
// TODO: scroll to selected function in reference
|
||||
// console.log('selectino change', selection.ranges[0].from);
|
||||
}, []);
|
||||
const handleTogglePlay = async () => {
|
||||
await getAudioContext().resume(); // fixes no sound in ios webkit
|
||||
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 (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
{!hideHeader && (
|
||||
<header
|
||||
id="header"
|
||||
className={cx(
|
||||
'flex-none w-full px-2 flex border-b border-gray-200 justify-between z-[10] bg-gray-100',
|
||||
isEmbedded ? 'h-8' : 'h-14',
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<img src={logo} className={cx('Tidal-logo', isEmbedded ? 'w-6 h-6' : 'w-10 h-10')} alt="logo" />
|
||||
<h1 className={isEmbedded ? 'text-l' : 'text-xl'}>Strudel {isEmbedded ? 'Mini ' : ''}REPL</h1>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<button
|
||||
onClick={async () => {
|
||||
await getAudioContext().resume(); // fixes no sound in ios webkit
|
||||
togglePlay();
|
||||
}}
|
||||
className={cx('hover:bg-gray-300', !isEmbedded ? 'p-2' : 'px-2')}
|
||||
>
|
||||
{!pending ? (
|
||||
<span className={cx('flex items-center', isEmbedded ? 'w-16' : 'w-16')}>
|
||||
{cycle.started ? (
|
||||
<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' }}
|
||||
/>
|
||||
// bg-gradient-to-t from-blue-900 to-slate-900
|
||||
// bg-gradient-to-t from-green-900 to-slate-900
|
||||
<AppContext.Provider
|
||||
value={{
|
||||
started,
|
||||
pending,
|
||||
isDirty,
|
||||
lastShared,
|
||||
activeCode,
|
||||
activeFooter,
|
||||
setActiveFooter,
|
||||
handleChangeCode,
|
||||
handleTogglePlay,
|
||||
handleUpdate,
|
||||
handleShuffle,
|
||||
handleShare,
|
||||
isZen,
|
||||
setIsZen,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={cx(
|
||||
'h-screen flex flex-col',
|
||||
// 'bg-gradient-to-t from-green-900 to-slate-900', //
|
||||
)}
|
||||
</section>
|
||||
{/* !isEmbedded && (
|
||||
<button className="fixed right-4 bottom-2 z-[11]" onClick={() => playStatic(code)}>
|
||||
static
|
||||
</button>
|
||||
) */}
|
||||
</div>
|
||||
>
|
||||
<Header />
|
||||
<section className="grow flex text-gray-100 relative overflow-auto cursor-text pb-0" id="code">
|
||||
<CodeMirror
|
||||
value={code}
|
||||
onChange={handleChangeCode}
|
||||
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 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) {
|
||||
// https://archive.org/details/SalamanderGrandPianoV3
|
||||
// License: CC-by http://creativecommons.org/licenses/by/3.0/ Author: Alexander Holm
|
||||
samples('piano.json', `${baseDir}/piano/`);
|
||||
// https://github.com/sgossner/VCSL/
|
||||
// https://api.github.com/repositories/126427031/contents/
|
||||
// LICENSE: CC0 general-purpose
|
||||
samples('vcsl.json', 'github:sgossner/VCSL/master/');
|
||||
samples('tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/');
|
||||
samples('EmuSP12.json', `${baseDir}/EmuSP12/`);
|
||||
return await Promise.all([
|
||||
samples('piano.json', `${baseDir}/piano/`),
|
||||
// https://github.com/sgossner/VCSL/
|
||||
// https://api.github.com/repositories/126427031/contents/
|
||||
// LICENSE: CC0 general-purpose
|
||||
samples('vcsl.json', 'github:sgossner/VCSL/master/'),
|
||||
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
|
||||
|
||||
// import * as tunes from './tunes.mjs';
|
||||
import { evaluate } from '@strudel.cycles/eval';
|
||||
// import { evaluate } from '@strudel.cycles/transpiler';
|
||||
// import { evaluate } from '@strudel.cycles/eval';
|
||||
import { evaluate } from '@strudel.cycles/transpiler';
|
||||
import { evalScope } from '@strudel.cycles/core';
|
||||
import * as strudel from '@strudel.cycles/core';
|
||||
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`] = `
|
||||
[
|
||||
"0/1 -> 3/2: {\\"s\\":\\"bd\\",\\"speed\\":0.7519542165100574}",
|
||||
|
||||
@ -308,7 +308,6 @@ export const blippyRhodes = `samples({
|
||||
}
|
||||
}, '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')])
|
||||
|
||||
stack(
|
||||
@ -950,7 +949,7 @@ export const flatrave = `stack(
|
||||
.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(
|
||||
// amen
|
||||
@ -981,3 +980,14 @@ stack(
|
||||
,
|
||||
n("0 1").s("east").delay(.5).degradeBy(.8).speed(rand.range(.5,1.5))
|
||||
).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 = {
|
||||
// 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: {
|
||||
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')],
|
||||
};
|
||||
|
||||
@ -7,7 +7,7 @@ export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
outDir: '../out',
|
||||
sourcemap: false,
|
||||
sourcemap: true,
|
||||
rollupOptions: {
|
||||
plugins: [visualizer({ template: 'treemap' })],
|
||||
},
|
||||
|
||||
@ -10,7 +10,7 @@ fetch('https://strudel.tidalcycles.org/EmuSP12.json')
|
||||
evalScope(
|
||||
controls,
|
||||
import('@strudel.cycles/core'),
|
||||
import('@strudel.cycles/tone'),
|
||||
// import('@strudel.cycles/tone'),
|
||||
import('@strudel.cycles/tonal'),
|
||||
import('@strudel.cycles/mini'),
|
||||
import('@strudel.cycles/midi'),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user