Compare commits

...

101 Commits

Author SHA1 Message Date
Felix Roos cd1c4632d7 build to single file 2022-11-23 14:00:31 +01:00
Felix Roos f1a850e133 forgot tonal pkg import 2022-11-22 21:43:30 +01:00
Felix Roos 53b61b8d85 add workshop seed project 2022-11-22 21:33:47 +01:00
Alex McLean e1a532500e Tidying up core (#256)
* remove _ prefixes except for functions to be patternified
* categorise pattern methods
* experimental support for `.add.squeeze` and friends as alternative to `.addSqueeze`
* `every` is now an alias for `firstOf` with additional `lastOf` (which every will become an alias for next)
2022-11-22 08:51:25 +00:00
Felix Roos 4cf412b93d Merge pull request #266 from tidalcycles/faster-fast
fix performance bottleneck
2022-11-21 22:15:48 +01:00
Felix Roos 54c9c434e0 fix examples snapshot 2022-11-21 22:09:26 +01:00
Felix Roos de19f3e5fe fix tune snapshots 2022-11-21 22:08:17 +01:00
Felix Roos 8304993481 fix: #194 2022-11-21 21:59:08 +01:00
Felix Roos 4c838aeaca hotfix: weird audion bug 2022-11-17 11:13:49 +01:00
Felix Roos 547d925065 Merge pull request #263 from tidalcycles/fix-tutorial-bugs
fix tutorial bugs
2022-11-17 11:08:37 +01:00
Felix Roos 874633051b fix: snapshot 2022-11-17 10:56:46 +01:00
Felix Roos 2293f1ba15 fix: worklets now work in the tutorial again 2022-11-17 10:52:07 +01:00
Felix Roos 483c3f61b5 midi now works in the tutorial 2022-11-17 10:49:44 +01:00
Felix Roos 83263968f2 fix: rootNotes example 2022-11-17 10:41:32 +01:00
Felix Roos f279c61792 fix: each example 2022-11-17 10:19:04 +01:00
Felix Roos 9717c696d8 add s for sharp (fixes notes example) 2022-11-17 10:16:29 +01:00
Felix Roos 94a594c777 fix sample examples 2022-11-17 10:15:59 +01:00
Felix Roos f1ae8a17cf fix: could play multiple mini repl at once 2022-11-17 10:07:19 +01:00
Felix Roos add9c43827 Merge pull request #254 from tidalcycles/binaries
Binaries
2022-11-16 13:15:28 +01:00
Felix Roos d3f6a5c296 package-lock 2022-11-16 13:13:50 +01:00
Felix Roos 4aea78b7c0 Merge branch 'main' into binaries 2022-11-16 13:13:11 +01:00
Felix Roos 4313fe670d hotfix: fix share #261 2022-11-13 21:32:46 +01:00
Felix Roos 2e8a1b67ea hotfix: fix prebuild 2022-11-13 20:53:28 +01:00
Felix Roos e73044c533 Publish
- @strudel.cycles/react@0.4.1
 - @strudel.cycles/soundfonts@0.4.1
 - @strudel.cycles/webaudio@0.4.1
2022-11-13 20:48:52 +01:00
Felix Roos 418f09e364 hotfix: remove webaudio build for now 2022-11-13 20:48:32 +01:00
Felix Roos 08f207a396 package-lock 2022-11-13 20:44:05 +01:00
Felix Roos 4f57060708 Publish
- @strudel.cycles/core@0.4.0
 - @strudel.cycles/eval@0.4.0
 - @strudel.cycles/midi@0.4.0
 - @strudel.cycles/mini@0.4.0
 - @strudel.cycles/osc@0.3.0
 - @strudel.cycles/react@0.4.0
 - @strudel.cycles/serial@0.2.0
 - @strudel.cycles/soundfonts@0.4.0
 - @strudel.cycles/tonal@0.4.0
 - @strudel.cycles/tone@0.4.0
 - @strudel.cycles/transpiler@0.4.0
 - @strudel.cycles/webaudio@0.4.0
 - @strudel.cycles/webdirt@0.4.0
 - @strudel.cycles/xen@0.4.0
2022-11-13 20:43:00 +01:00
Felix Roos 1507724ecd hotfix: webaudio main file 2022-11-13 20:42:17 +01:00
Felix Roos 4bc2f64b24 Merge pull request #255 from tidalcycles/repl-refactoring
Repl refactoring
2022-11-13 20:20:32 +01:00
Felix Roos 839ba31dfb remove refactoring notes 2022-11-13 18:49:16 +01:00
Felix Roos 2341452d91 fix: highlighting error 2022-11-13 18:44:00 +01:00
Felix Roos 7ab263b514 floating error message 2022-11-13 17:11:14 +01:00
Felix Roos 45b5b5960f remove hideHeader function 2022-11-13 16:54:45 +01:00
Felix Roos 1b8ec1f01c secret zen mode 2022-11-13 15:47:11 +01:00
Felix Roos c92cb1c096 better draw cleanup + began reference tab 2022-11-13 14:50:48 +01:00
Felix Roos 8f31fbe275 proper draw cleanup 2022-11-13 13:58:36 +01:00
Felix Roos aa76419eda fix activeFooter state 2022-11-13 12:25:30 +01:00
Felix Roos 21503c0c38 separate header + footer components 2022-11-13 12:12:19 +01:00
Felix Roos d957b19f55 fix first highlights 2022-11-13 02:00:09 +01:00
Felix Roos 122c4242ce use new transpiler in test runtime 2022-11-13 01:43:54 +01:00
Felix Roos e769ed2fd8 move draw stuff from tone to core + fix getPhase 2022-11-13 01:42:00 +01:00
Felix Roos c722904831 prevent flicker 2022-11-13 00:30:48 +01:00
Felix Roos c93faa9564 show intro only when coming to root url 2022-11-13 00:30:18 +01:00
Felix Roos ce6495a46d unfriend intro readers 2022-11-12 23:38:02 +01:00
Felix Roos 04f2b905af basic intro tab 2022-11-12 21:28:07 +01:00
Felix Roos b668a2c0d2 refactor onTrigger 2022-11-12 20:17:57 +01:00
Felix Roos 5fc8f10602 basic samples tab 2022-11-12 20:17:01 +01:00
Felix Roos a27f399a9e tabbed footer 2022-11-12 17:50:10 +01:00
Felix Roos ff5d14fb63 can now have multiple triggers
+ Pattern.log now makes sense
2022-11-12 16:52:00 +01:00
Felix Roos 27fb2d2d5b remove onLog callbacks 2022-11-12 12:07:02 +01:00
Felix Roos 0b9d59bf0e logger works now outside of react
+ dynamic sample loading logs
+ remove old sampler code
2022-11-12 11:50:39 +01:00
Felix Roos 23e059a065 scheduler error handling + style 2022-11-11 23:35:46 +01:00
Felix Roos 45c7b29a96 big style update + new log system 2022-11-11 22:46:20 +01:00
Felix Roos 3e6c3fda60 update todos 2022-11-10 22:30:12 +01:00
Felix Roos 1a1363ef77 scheduler error handling 2022-11-10 22:25:58 +01:00
Felix Roos b5c51e231b lazy midi init + remove useWebMidi hook 2022-11-10 22:25:28 +01:00
Felix Roos 22711dd7be remove log 2022-11-10 22:05:08 +01:00
Felix Roos 1ba4d3d5d1 only connect via osc when .osc is called 2022-11-10 22:04:22 +01:00
Felix Roos 823ab73585 cleaner error when midi is unsupported 2022-11-10 22:04:02 +01:00
Felix Roos 66c444efac add sourcemaps 2022-11-10 22:03:34 +01:00
Felix Roos b688b8921a init audio on first click to remove warning 2022-11-10 22:03:23 +01:00
Felix Roos 00550ddab5 pass all params to onTrigger 2022-11-10 22:02:08 +01:00
Felix Roos 42fedec088 fix logger 2022-11-10 22:01:55 +01:00
Felix Roos ff3bc0e8c2 fix core log 2022-11-10 22:01:49 +01:00
Felix Roos dde7dff870 fix: hash urls 2022-11-10 21:11:25 +01:00
Felix Roos 25ffc9cf96 add logger 2022-11-10 21:10:33 +01:00
Felix Roos eca92cf5b5 fix: web midi 2022-11-10 18:12:38 +01:00
Felix Roos 372bdf8be7 fix: highlighting sync 2022-11-10 17:15:10 +01:00
Felix Roos c7c90b0647 fix mini repl + improve repl api 2022-11-10 16:36:51 +01:00
Felix Roos 14c2da4fa2 started refac repl to new scheduler + transpiler 2022-11-10 12:07:36 +01:00
Felix Roos 64275cfd22 revert server changes 2022-11-10 10:12:52 +01:00
Felix Roos f0b099d9f7 Merge remote-tracking branch 'origin/main' into binaries 2022-11-10 10:11:47 +01:00
Felix Roos c99d957bc8 Merge pull request #250 from tidalcycles/webaudio-build
Webaudio build
2022-11-08 23:35:10 +01:00
Felix Roos 51b14d55ec fix webaudio main file + add buildless repl 2022-11-08 23:29:27 +01:00
Felix Roos f92d9994df Publish
- @strudel.cycles/soundfonts@0.3.3
 - @strudel.cycles/webaudio@0.3.3
2022-11-08 23:17:43 +01:00
Felix Roos 431e1b87bd add webaudio cjs + esm build task 2022-11-08 23:16:24 +01:00
Felix Roos 3ec2fc6030 Publish
- @strudel.cycles/midi@0.3.3
 - @strudel.cycles/mini@0.3.3
 - @strudel.cycles/react@0.3.3
 - @strudel.cycles/tonal@0.3.3
 - @strudel.cycles/tone@0.3.3
2022-11-08 22:52:46 +01:00
Felix Roos 99f416bf6b hotfix: fix chord-voicings dependency 2022-11-08 22:51:28 +01:00
Felix Roos 89acc4870b Publish
- @strudel.cycles/core@0.3.2
 - @strudel.cycles/eval@0.3.2
 - @strudel.cycles/midi@0.3.2
 - @strudel.cycles/mini@0.3.2
 - @strudel.cycles/react@0.3.2
 - @strudel.cycles/soundfonts@0.3.2
 - @strudel.cycles/tonal@0.3.2
 - @strudel.cycles/tone@0.3.2
 - @strudel.cycles/transpiler@0.3.2
 - @strudel.cycles/webaudio@0.3.2
 - @strudel.cycles/webdirt@0.3.2
 - @strudel.cycles/xen@0.3.2
2022-11-08 22:45:44 +01:00
Felix Roos 314c739c3a Merge pull request #249 from tidalcycles/use-acorn
new transpiler based on acorn
2022-11-08 22:43:31 +01:00
Felix Roos 30b18fc997 add transpiler readme 2022-11-08 22:32:42 +01:00
Felix Roos 168ac60853 use tune that works without transpiler 2022-11-08 21:25:21 +01:00
Felix Roos dff0c0c3b0 vanilla-repl: use mini for all strings
+ skip transpiler
2022-11-08 21:17:00 +01:00
Felix Roos ea3e9de7ab feat: can now set a string parser for reify 2022-11-08 21:16:23 +01:00
Felix Roos c93fd900bc fix: tests 2022-11-08 21:04:41 +01:00
Felix Roos a761aa9a37 use new transpiler in nano-repl 2022-11-08 20:53:47 +01:00
Felix Roos f37eb92d93 support line,column,offset format 2022-11-08 20:53:26 +01:00
Felix Roos 2c7cc5d325 core evaluate now works without transpiler 2022-11-08 20:23:30 +01:00
Felix Roos 16659c82d2 test with old transpiler for now 2022-11-08 19:54:57 +01:00
Felix Roos 4ed3ca4128 use new transpiler in vite-vanilla-repl 2022-11-08 19:50:27 +01:00
Felix Roos 9b2899c8fb transpiler package to replace eval package 2022-11-06 23:15:01 +01:00
Felix Roos fada7fc9cc Publish
- @strudel.cycles/eval@0.3.1
 - @strudel.cycles/midi@0.3.1
 - @strudel.cycles/mini@0.3.1
 - @strudel.cycles/react@0.3.1
 - @strudel.cycles/soundfonts@0.3.1
 - @strudel.cycles/tonal@0.3.1
 - @strudel.cycles/tone@0.3.1
 - @strudel.cycles/webaudio@0.3.1
 - @strudel.cycles/webdirt@0.3.1
 - @strudel.cycles/xen@0.3.1
2022-11-06 19:53:24 +01:00
Felix Roos 2be5bca42b hotfix: core dependencies 2022-11-06 19:51:56 +01:00
Felix Roos b1ebf6367c Publish
- @strudel.cycles/core@0.3.1
 - @strudel.cycles/eval@0.3.0
 - @strudel.cycles/midi@0.3.0
 - @strudel.cycles/mini@0.3.0
 - @strudel.cycles/react@0.3.0
 - @strudel.cycles/soundfonts@0.3.0
 - @strudel.cycles/tonal@0.3.0
 - @strudel.cycles/tone@0.3.0
 - @strudel.cycles/webaudio@0.3.0
 - @strudel.cycles/webdirt@0.3.0
 - @strudel.cycles/xen@0.3.0
2022-11-06 19:39:30 +01:00
Felix Roos 57e373f578 hotfix: npmignore react examples 2022-11-06 19:39:05 +01:00
Felix Roos 5c01874a32 release: bump core 0.3.0 2022-11-06 19:37:49 +01:00
Felix Roos dd89e11633 hotfix: npm ignore examples folder 2022-11-06 19:36:01 +01:00
Felix Roos 4e51f19780 Publish
- @strudel.cycles/embed@0.2.0
 - @strudel.cycles/osc@0.2.0
2022-11-06 19:30:53 +01:00
Felix Roos 33d37d8dee Revert "Publish"
This reverts commit 60ce34af43.
2022-11-06 19:28:11 +01:00
Felix Roos ad3a324719 Merge branch 'in-source-doc' into binaries 2022-05-25 21:55:53 +02:00
Felix Roos 8c9e178206 add build script for binaries 2022-05-25 21:24:05 +02:00
125 changed files with 11681 additions and 4202 deletions
+1 -1
View File
@@ -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": {
+745 -114
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -10,7 +10,7 @@
"test-coverage": "vitest --coverage",
"bootstrap": "lerna bootstrap",
"setup": "npm i && npm run bootstrap && cd repl && npm i && cd ../tutorial && npm i",
"snapshot": "cd repl && npm run snapshot",
"snapshot": "vitest run -u --silent",
"repl": "cd repl && npm run dev",
"osc": "cd packages/osc && npm run server",
"build": "rm -rf out && cd repl && npm run build && cd ../tutorial && npm run build",
+1
View File
@@ -0,0 +1 @@
examples
+17 -11
View File
@@ -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);
}
+14 -6
View File
@@ -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) => {
@@ -24,8 +22,16 @@ export const evalScope = async (...args) => {
Object.assign(globalThis, ...modules, Pattern.prototype.bootstrap());
};
function safeEval(str) {
return Function('"use strict";return (' + str + ')')();
function safeEval(str, options = {}) {
const { wrapExpression = true, wrapAsync = true } = options;
if (wrapExpression) {
str = `{${str}}`;
}
if (wrapAsync) {
str = `(async ()=>${str})()`;
}
const body = `"use strict";return (${str})`;
return Function(body)();
}
export const evaluate = async (code, transpiler) => {
@@ -35,7 +41,9 @@ export const evaluate = async (code, transpiler) => {
if (transpiler) {
code = transpiler(code); // transform syntactically correct js code to semantically usable code
}
let evaluated = await safeEval(code);
// if no transpiler is given, we expect a single instruction (!wrapExpression)
const options = { wrapExpression: !!transpiler };
let evaluated = await safeEval(code, options);
if (!isPattern(evaluated)) {
console.log('evaluated', evaluated);
const message = `got "${typeof evaluated}" instead of pattern`;
+96
View File
@@ -0,0 +1,96 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Buildless Vanilla Strudel REPL</title>
</head>
<body style="margin: 0; background: #222">
<div style="display: grid; height: 100vh">
<textarea
id="text"
style="font-size: 2em; border: 0; color: white; background: transparent; outline: none; padding: 20px"
spellcheck="false"
></textarea>
</div>
<button
id="start"
style="
position: absolute;
border-radius: 10px;
top: 20px;
right: 20px;
padding: 20px;
border: 2px solid white;
background: transparent;
color: white;
cursor: pointer;
"
>
evaluate
</button>
<div id="output"></div>
<script type="module">
import { controls, repl, evalScope } from 'https://cdn.skypack.dev/@strudel.cycles/core@0.3.2';
import { mini } from 'https://cdn.skypack.dev/@strudel.cycles/mini@0.3.2';
import { transpiler } from 'https://cdn.skypack.dev/@strudel.cycles/transpiler@0.3.2';
import { getAudioContext, webaudioOutput } from 'https://cdn.skypack.dev/@strudel.cycles/webaudio@0.3.3';
const ctx = getAudioContext();
const input = document.getElementById('text');
input.innerHTML = getTune();
evalScope(
controls,
import('https://cdn.skypack.dev/@strudel.cycles/core@0.3.2'),
import('https://cdn.skypack.dev/@strudel.cycles/mini@0.3.2'),
import('https://cdn.skypack.dev/@strudel.cycles/tonal@0.3.3'),
import('https://cdn.skypack.dev/@strudel.cycles/webaudio@0.3.3'),
);
const { evaluate } = repl({
defaultOutput: webaudioOutput,
getTime: () => ctx.currentTime,
transpiler,
});
document.getElementById('start').addEventListener('click', () => {
ctx.resume();
evaluate(input.value);
});
function getTune() {
return `await samples('github:tidalcycles/Dirt-Samples/master')
stack(
// amen
n("0 1 2 3 4 5 6 7")
.sometimes(x=>x.ply(2))
.rarely(x=>x.speed("2 | -2"))
.sometimesBy(.4, x=>x.delay(".5"))
.s("amencutup")
.slow(2)
.room(.5)
,
// bass
sine.add(saw.slow(4)).range(0,7).segment(8)
.superimpose(x=>x.add(.1))
.scale('G0 minor').note()
.s("sawtooth").decay(.1).sustain(0)
.gain(.4).cutoff(perlin.range(300,3000).slow(8)).resonance(10)
.degradeBy("0 0.1 .5 .1")
.rarely(add(note("12")))
,
// chord
note("Bb3,D4".superimpose(x=>x.add(.2)))
.s('sawtooth').cutoff(1000).struct("<~@3 [~ x]>")
.decay(.05).sustain(.0).delay(.8).delaytime(.125).room(.8)
,
// alien
s("breath").room(1).shape(.6).chop(16).rev().mask("<x ~@7>")
,
n("0 1").s("east").delay(.5).degradeBy(.8).speed(rand.range(.5,1.5))
).reset("<x@7 x(5,8)>")`;
}
</script>
</body>
</html>
@@ -0,0 +1 @@
import{b as s,h as i,m,a as n,c as p,p as t}from"./index.4cbc0a10.js";export{s as SyntaxError,i as h,m as mini,n as minify,p as parse,t as patternifyAST};
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
import{g as s,b as d,e as t,j as u,l as o,f as r,p as f,i as p,d as g,h as i,w as l,a as m}from"./index.f88145a1.js";export{s as getAudioContext,d as getCachedBuffer,t as getLoadedBuffer,u as getLoadedSamples,o as loadBuffer,r as loadGithubSamples,f as panic,p as resetLoadedSamples,g as reverseBuffer,i as samples,l as webaudioOutput,m as webaudioOutputTrigger};
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
import{g as s,f as d,i as t,n as r,l as u,j as o,d as f,k as p,r as g,s as i,w as l,e as m}from"./index.4cbc0a10.js";export{s as getAudioContext,d as getCachedBuffer,t as getLoadedBuffer,r as getLoadedSamples,u as loadBuffer,o as loadGithubSamples,f as panic,p as resetLoadedSamples,g as reverseBuffer,i as samples,l as webaudioOutput,m as webaudioOutputTrigger};
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite Vanilla Strudel REPL</title>
<script type="module" crossorigin src="/tidalcycles/strudel/general-purpose-scheduler/packages/core/examples/vite-vanilla-repl/dist/assets/index.f88145a1.js"></script>
<script type="module" crossorigin src="/tidalcycles/strudel/use-acorn/packages/core/examples/vite-vanilla-repl/dist/assets/index.4cbc0a10.js"></script>
</head>
<body style="margin: 0; background: #222">
<div style="display: grid; height: 100vh">
@@ -1,6 +1,7 @@
import { controls, repl, evalScope } from '@strudel.cycles/core';
import { controls, repl, evalScope, setStringParser } from '@strudel.cycles/core';
import { mini } from '@strudel.cycles/mini';
import { getAudioContext, webaudioOutput } from '@strudel.cycles/webaudio';
import shapeshifter from '@strudel.cycles/eval/shapeshifter.mjs';
// import { transpiler } from '@strudel.cycles/transpiler';
import tune from './tune.mjs';
const ctx = getAudioContext();
@@ -15,10 +16,12 @@ evalScope(
import('@strudel.cycles/tonal'),
);
setStringParser(mini)
const { evaluate } = repl({
defaultOutput: webaudioOutput,
getTime: () => ctx.currentTime,
transpiler: shapeshifter,
// transpiler,
});
document.getElementById('start').addEventListener('click', () => {
ctx.resume();
@@ -5,10 +5,11 @@
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build --base /tidalcycles/strudel/general-purpose-scheduler/packages/core/examples/vite-vanilla-repl/dist/",
"build-githack": "vite build --base /tidalcycles/strudel/use-acorn/packages/core/examples/vite-vanilla-repl/dist/",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"vite": "^3.2.0"
}
}
}
@@ -1,4 +1,4 @@
export default `await samples('github:tidalcycles/Dirt-Samples/master')
/* export default `await samples('github:tidalcycles/Dirt-Samples/master')
stack(
// amen
@@ -29,3 +29,11 @@ stack(
,
n("0 1").s("east").delay(.5).degradeBy(.8).speed(rand.range(.5,1.5))
).reset("<x@7 x(5,8)>")`;
*/
export default `stack(
n("c3 [eb3,g3]")
.delay("<0 .5>")
.delaytime(".16 | .33")
.delayfeedback(".6 | .8")
).sometimes(x=>x.speed("-1"))`;
+3 -2
View File
@@ -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;
};
@@ -73,7 +74,7 @@ const fraction = (n) => {
-> those farey sequences turn out to make pattern querying ~20 times slower! always use strings!
-> still, some optimizations could be done: .mul .div .add .sub calls still use numbers
*/
n = String(n);
// n = String(n); // this is actually faster but imprecise...
}
return Fraction(n);
};
+6 -2
View File
@@ -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 -4
View File
@@ -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
View 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;
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/core",
"version": "0.3.0",
"version": "0.4.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/core",
"version": "0.3.0",
"version": "0.4.0",
"description": "Port of Tidal Cycles to JavaScript",
"main": "index.mjs",
"type": "module",
+654 -571
View File
File diff suppressed because it is too large Load Diff
@@ -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) => {
+46 -10
View File
@@ -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 };
}
+7 -7
View File
@@ -20,7 +20,7 @@ export const signal = (func) => {
};
export const isaw = signal((t) => 1 - (t % 1));
export const isaw2 = isaw._toBipolar();
export const isaw2 = isaw.toBipolar();
/**
* A sawtooth signal between 0 and 1.
@@ -33,7 +33,7 @@ export const isaw2 = isaw._toBipolar();
*
*/
export const saw = signal((t) => t % 1);
export const saw2 = saw._toBipolar();
export const saw2 = saw.toBipolar();
export const sine2 = signal((t) => Math.sin(Math.PI * 2 * t));
@@ -45,7 +45,7 @@ export const sine2 = signal((t) => Math.sin(Math.PI * 2 * t));
* sine.segment(16).range(0,15).slow(2).scale('C minor').note()
*
*/
export const sine = sine2._fromBipolar();
export const sine = sine2.fromBipolar();
/**
* A cosine signal between 0 and 1.
@@ -67,7 +67,7 @@ export const cosine2 = sine2._early(Fraction(1).div(4));
*
*/
export const square = signal((t) => Math.floor((t * 2) % 2));
export const square2 = square._toBipolar();
export const square2 = square.toBipolar();
/**
* A triangle signal between 0 and 1.
@@ -127,7 +127,7 @@ export const rand = signal(timeToRand);
/**
* A continuous pattern of random numbers, between -1 and 1
*/
export const rand2 = rand._toBipolar();
export const rand2 = rand.toBipolar();
export const _brandBy = (p) => rand.fmap((x) => x < p);
export const brandBy = (pPat) => reify(pPat).fmap(_brandBy).innerJoin();
@@ -201,7 +201,7 @@ Pattern.prototype.choose = function (...xs) {
* @returns {Pattern}
*/
Pattern.prototype.choose2 = function (...xs) {
return chooseWith(this._fromBipolar(), xs);
return chooseWith(this.fromBipolar(), xs);
};
/**
@@ -259,7 +259,7 @@ export const perlinWith = (pat) => {
export const perlin = perlinWith(time.fmap((v) => Number(v)));
Pattern.prototype._degradeByWith = function (withPat, x) {
return this.fmap((a) => (_) => a).appLeft(withPat._filterValues((v) => v > x));
return this.fmap((a) => (_) => a).appLeft(withPat.filterValues((v) => v > x));
};
/**
+2 -5
View File
@@ -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);
});
};
+32 -25
View File
@@ -49,7 +49,7 @@ import { steady } from '../signal.mjs';
import controls from '../controls.mjs';
const { n } = controls;
const { n, s } = controls;
const st = (begin, end) => new State(ts(begin, end));
const ts = (begin, end) => new TimeSpan(Fraction(begin), Fraction(end));
const hap = (whole, part, value, context = {}) => new Hap(whole, part, value, context);
@@ -58,7 +58,7 @@ const third = Fraction(1, 3);
const twothirds = Fraction(2, 3);
const sameFirst = (a, b) => {
return expect(a._sortHapsByPart().firstCycle()).toStrictEqual(b._sortHapsByPart().firstCycle());
return expect(a.sortHapsByPart().firstCycle()).toStrictEqual(b.sortHapsByPart().firstCycle());
};
describe('TimeSpan', () => {
@@ -382,7 +382,7 @@ describe('Pattern', () => {
);
});
it('copes with breaking up events across cycles', () => {
expect(pure('a').slow(2)._fastGap(2)._setContext({}).query(st(0, 2))).toStrictEqual([
expect(pure('a').slow(2)._fastGap(2).setContext({}).query(st(0, 2))).toStrictEqual([
hap(ts(0, 1), ts(0, 0.5), 'a'),
hap(ts(0.5, 1.5), ts(1, 1.5), 'a'),
]);
@@ -446,8 +446,8 @@ describe('Pattern', () => {
});
describe('slow()', () => {
it('Supports zero-length queries', () => {
expect(steady('a').slow(1)._setContext({}).queryArc(0, 0)).toStrictEqual(
steady('a')._setContext({}).queryArc(0, 0),
expect(steady('a').slow(1).setContext({}).queryArc(0, 0)).toStrictEqual(
steady('a').setContext({}).queryArc(0, 0),
);
});
});
@@ -465,7 +465,7 @@ describe('Pattern', () => {
it('Filters true', () => {
expect(
pure(true)
._filterValues((x) => x)
.filterValues((x) => x)
.firstCycle().length,
).toBe(1);
});
@@ -490,7 +490,7 @@ describe('Pattern', () => {
pure(10)
.when(slowcat(true, false), (x) => x.add(3))
.fast(4)
._sortHapsByPart()
.sortHapsByPart()
.firstCycle(),
).toStrictEqual(fastcat(13, 10, 13, 10).firstCycle());
});
@@ -577,26 +577,26 @@ describe('Pattern', () => {
});
});
describe('every()', () => {
describe('firstOf()', () => {
it('Can apply a function every 3rd time', () => {
expect(
pure('a')
.every(3, (x) => x._fast(2))
.firstOf(3, (x) => x._fast(2))
._fast(3)
.firstCycle(),
).toStrictEqual(sequence(sequence('a', 'a'), 'a', 'a').firstCycle());
});
it('works with currying', () => {
expect(pure('a').every(3, fast(2))._fast(3).firstCycle()).toStrictEqual(
expect(pure('a').firstOf(3, fast(2))._fast(3).firstCycle()).toStrictEqual(
sequence(sequence('a', 'a'), 'a', 'a').firstCycle(),
);
expect(sequence(3, 4, 5).every(3, add(3)).fast(5).firstCycle()).toStrictEqual(
expect(sequence(3, 4, 5).firstOf(3, add(3)).fast(5).firstCycle()).toStrictEqual(
sequence(6, 7, 8, 3, 4, 5, 3, 4, 5, 6, 7, 8, 3, 4, 5).firstCycle(),
);
expect(sequence(3, 4, 5).every(2, sub(1)).fast(5).firstCycle()).toStrictEqual(
expect(sequence(3, 4, 5).firstOf(2, sub(1)).fast(5).firstCycle()).toStrictEqual(
sequence(2, 3, 4, 3, 4, 5, 2, 3, 4, 3, 4, 5, 2, 3, 4).firstCycle(),
);
expect(sequence(3, 4, 5).every(3, add(3)).every(2, sub(1)).fast(2).firstCycle()).toStrictEqual(
expect(sequence(3, 4, 5).firstOf(3, add(3)).firstOf(2, sub(1)).fast(2).firstCycle()).toStrictEqual(
sequence(5, 6, 7, 3, 4, 5).firstCycle(),
);
});
@@ -692,7 +692,7 @@ describe('Pattern', () => {
it('Can set the hap context', () => {
expect(
pure('a')
._setContext([
.setContext([
[
[0, 1],
[1, 2],
@@ -713,13 +713,13 @@ describe('Pattern', () => {
it('Can update the hap context', () => {
expect(
pure('a')
._setContext([
.setContext([
[
[0, 1],
[1, 2],
],
])
._withContext((c) => [
.withContext((c) => [
...c,
[
[3, 4],
@@ -743,7 +743,7 @@ describe('Pattern', () => {
});
describe('apply', () => {
it('Can apply a function', () => {
expect(sequence('a', 'b')._apply(fast(2)).firstCycle()).toStrictEqual(sequence('a', 'b').fast(2).firstCycle());
expect(sequence('a', 'b').apply(fast(2)).firstCycle()).toStrictEqual(sequence('a', 'b').fast(2).firstCycle());
}),
it('Can apply a pattern of functions', () => {
expect(sequence('a', 'b').apply(fast(2)).firstCycle()).toStrictEqual(sequence('a', 'b').fast(2).firstCycle());
@@ -784,18 +784,18 @@ describe('Pattern', () => {
});
describe('jux', () => {
it('Can juxtapose', () => {
expect(pure({ a: 1 }).jux(fast(2))._sortHapsByPart().firstCycle()).toStrictEqual(
expect(pure({ a: 1 }).jux(fast(2)).sortHapsByPart().firstCycle()).toStrictEqual(
stack(pure({ a: 1, pan: 0 }), pure({ a: 1, pan: 1 }).fast(2))
._sortHapsByPart()
.sortHapsByPart()
.firstCycle(),
);
});
});
describe('juxBy', () => {
it('Can juxtapose by half', () => {
expect(pure({ a: 1 }).juxBy(0.5, fast(2))._sortHapsByPart().firstCycle()).toStrictEqual(
expect(pure({ a: 1 }).juxBy(0.5, fast(2)).sortHapsByPart().firstCycle()).toStrictEqual(
stack(pure({ a: 1, pan: 0.25 }), pure({ a: 1, pan: 0.75 }).fast(2))
._sortHapsByPart()
.sortHapsByPart()
.firstCycle(),
);
});
@@ -805,7 +805,7 @@ describe('Pattern', () => {
expect(
sequence('a', ['a', 'a'])
.fmap((a) => fastcat('b', 'c'))
._squeezeJoin()
.squeezeJoin()
.firstCycle(),
).toStrictEqual(
sequence(
@@ -820,7 +820,7 @@ describe('Pattern', () => {
it('Squeezes to the correct cycle', () => {
expect(
pure(time.struct(true))
._squeezeJoin()
.squeezeJoin()
.queryArc(3, 4)
.map((x) => x.value),
).toStrictEqual([Fraction(3.5)]);
@@ -857,7 +857,7 @@ describe('Pattern', () => {
);
});
it('Can chop(2,3)', () => {
expect(pure({ sound: 'a' }).fast(2).chop(2, 3)._sortHapsByPart().firstCycle()).toStrictEqual(
expect(pure({ sound: 'a' }).fast(2).chop(2, 3).sortHapsByPart().firstCycle()).toStrictEqual(
sequence(
[
{ sound: 'a', begin: 0, end: 0.5 },
@@ -869,7 +869,7 @@ describe('Pattern', () => {
{ sound: 'a', begin: 2 / 3, end: 1 },
],
)
._sortHapsByPart()
.sortHapsByPart()
.firstCycle(),
);
});
@@ -893,4 +893,11 @@ describe('Pattern', () => {
);
});
});
describe('alignments', () => {
it('Can squeeze arguments', () => {
expect(sequence(1, 2).add.squeeze(4, 5).firstCycle()).toStrictEqual(
sequence(5, 6, 6, 7).firstCycle()
);
});
});
});
+11
View 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
}
};
+6 -2
View File
@@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
*/
// returns true if the given string is a note
export const isNote = (name) => /^[a-gA-G][#b]*[0-9]$/.test(name);
export const isNote = (name) => /^[a-gA-G][#bs]*[0-9]$/.test(name);
export const tokenizeNote = (note) => {
if (typeof note !== 'string') {
return [];
@@ -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') {
+2 -1
View File
@@ -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;
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/eval",
"version": "0.3.0",
"version": "0.4.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/eval",
"version": "0.3.0",
"version": "0.4.0",
"description": "Code evaluator for strudel",
"main": "index.mjs",
"type": "module",
@@ -28,7 +28,7 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "^0.3.0",
"@strudel.cycles/core": "^0.4.0",
"estraverse": "^5.3.0",
"shift-ast": "^7.0.0",
"shift-codegen": "^8.1.0",
+4 -3
View File
@@ -30,7 +30,8 @@ const isNote = (name) => /^[a-gC-G][bs]?[0-9]$/.test(name);
const addLocations = true;
export const addMiniLocations = true;
export const minifyStrings = true;
export const wrappedAsync = true;
export const wrappedAsync = false; // this is now handled by core evaluate by default
export const shouldAddReturn = true;
export default (_code) => {
const { code, addReturn } = wrapAsync(_code);
@@ -125,7 +126,7 @@ export default (_code) => {
},
});
// add return to last statement (because it's wrapped in an async function artificially)
if (wrappedAsync) {
if (shouldAddReturn) {
addReturn(shifted);
}
const generated = undisguiseImports(codegen(shifted));
@@ -153,7 +154,7 @@ ${code}
})()`;
}
const addReturn = (ast) => {
const body = ast.statements[0].expression.callee.body; // actual code ast inside async function body
const body = wrappedAsync ? ast.statements[0].expression.callee.body : ast;
body.statements = body.statements
.slice(0, -1)
.concat([new ReturnStatement({ expression: body.statements.slice(-1)[0] })]);
+1 -1
View File
@@ -13,7 +13,7 @@ const { fastcat, evalScope } = strudel;
describe('evaluate', async () => {
await evalScope({ mini }, strudel);
const ev = async (code) => (await evaluate(code)).pattern._firstCycleValues;
const ev = async (code) => (await evaluate(code)).pattern.firstCycleValues;
it('Should evaluate strudel functions', async () => {
expect(await ev('pure("c3")')).toEqual(['c3']);
expect(await ev('cat("c3")')).toEqual(['c3']);
+13 -7
View File
@@ -5,15 +5,21 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import { describe, it, expect } from 'vitest';
import shapeshifter from '../shapeshifter.mjs';
import shapeshifter, { wrappedAsync } from '../shapeshifter.mjs';
describe('shapeshifter', () => {
it('Should shift simple double quote string', () => {
expect(shapeshifter('"c3"')).toEqual('(async()=>{return mini("c3").withMiniLocation([1,0,15],[1,4,19])})()');
});
it('Should handle dynamic imports', () => {
expect(shapeshifter('const { default: foo } = await import(\'https://bar.com/foo.js\');"c3"')).toEqual(
'(async()=>{const{default:foo}=await import("https://bar.com/foo.js");return mini("c3").withMiniLocation([1,64,79],[1,68,83])})()',
);
if (wrappedAsync) {
expect(shapeshifter('"c3"')).toEqual('(async()=>{return mini("c3").withMiniLocation([1,0,15],[1,4,19])})()');
} else {
expect(shapeshifter('"c3"')).toEqual('return mini("c3").withMiniLocation([1,0,0],[1,4,4])');
}
});
if (wrappedAsync) {
it('Should handle dynamic imports', () => {
expect(shapeshifter('const { default: foo } = await import(\'https://bar.com/foo.js\');"c3"')).toEqual(
'const{default:foo}=await import("https://bar.com/foo.js");return mini("c3").withMiniLocation([1,64,79],[1,68,83])',
);
});
}
});
+69 -46
View File
@@ -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,
});
});
};
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/midi",
"version": "0.3.0",
"version": "0.4.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/midi",
"version": "0.3.0",
"version": "0.4.0",
"description": "Midi API for strudel",
"main": "index.mjs",
"repository": {
@@ -21,7 +21,7 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/tone": "^0.3.0",
"@strudel.cycles/tone": "^0.4.0",
"tone": "^14.7.77",
"webmidi": "^3.0.21"
}
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/mini",
"version": "0.3.0",
"version": "0.4.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/mini",
"version": "0.3.0",
"version": "0.4.0",
"description": "Mini notation for strudel",
"main": "index.mjs",
"type": "module",
@@ -26,9 +26,9 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "^0.3.0",
"@strudel.cycles/eval": "^0.3.0",
"@strudel.cycles/tone": "^0.3.0"
"@strudel.cycles/core": "^0.4.0",
"@strudel.cycles/eval": "^0.4.0",
"@strudel.cycles/tone": "^0.4.0"
},
"devDependencies": {
"peggy": "^2.0.1"
+2 -2
View File
@@ -9,8 +9,8 @@ import '@strudel.cycles/core/euclid.mjs';
import { describe, expect, it } from 'vitest';
describe('mini', () => {
const minV = (v) => mini(v)._firstCycleValues;
const minS = (v) => mini(v)._showFirstCycle;
const minV = (v) => mini(v).firstCycleValues;
const minS = (v) => mini(v).showFirstCycle;
it('supports single elements', () => {
expect(minV('a')).toEqual(['a']);
});
+47 -26
View File
@@ -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);
});
};
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/osc",
"version": "0.2.0",
"version": "0.3.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
+6 -2
View File
@@ -1,13 +1,14 @@
{
"name": "@strudel.cycles/osc",
"version": "0.2.0",
"version": "0.3.0",
"description": "OSC messaging for strudel",
"main": "osc.mjs",
"scripts": {
"test": "echo \"No tests present.\" && exit 0",
"server": "node server.js",
"tidal-sniffer": "node tidal-sniffer.js",
"client": "npx serve -p 4321"
"client": "npx serve -p 4321",
"build": "npx pkg server.js --targets node16-macos-x64,node16-win-x64,node16-linux-x64 --out-path bin"
},
"repository": {
"type": "git",
@@ -31,5 +32,8 @@
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"osc-js": "^2.4.0"
},
"devDependencies": {
"pkg": "^5.7.0"
}
}
+1
View File
@@ -0,0 +1 @@
examples
+1 -3
View File
File diff suppressed because one or more lines are too long
+246 -394
View File
@@ -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 _, useRef as H, useEffect as L, useMemo as V, useState as w, useLayoutEffect as j } from "react";
import X from "@uiw/react-codemirror";
import { Decoration as E, EditorView as U } from "@codemirror/view";
import { StateEffect as $, StateField as G } from "@codemirror/state";
import { javascript as Y } from "@codemirror/lang-javascript";
import { tags as r } from "@lezer/highlight";
import { createTheme as Z } from "@uiw/codemirror-themes";
import { useInView as ee } from "react-hook-inview";
import { webaudioOutput as te, getAudioContext as re } from "@strudel.cycles/webaudio";
import { repl as oe } from "@strudel.cycles/core";
import { transpiler as ne } from "@strudel.cycles/transpiler";
const ae = Z({
theme: "dark",
settings: {
background: "#222",
@@ -19,450 +17,304 @@ 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 B = $.define(), se = G.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(B))
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) => U.decorations.from(e)
}), ce = (e) => {
e.dispatch({ effects: B.of(!0) }), setTimeout(() => {
e.dispatch({ effects: B.of(!1) });
}, 200);
}, O = U.define(), le = Q.define({
}, z = $.define(), ie = G.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(z)) {
const a = o.value.map(
(s) => (s.context.locations || []).map(({ start: f, end: d }) => {
const u = s.context.color || "#FFCA28";
let c = t.newDoc.line(f.line).from + f.column, i = t.newDoc.line(d.line).from + d.column;
const m = t.newDoc.length;
return c > m || i > m ? void 0 : E.mark({ attributes: { style: `outline: 1.5px solid ${u};` } }).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) => U.decorations.from(e)
}), le = [Y(), ae, ie, se];
function de({ value: e, onChange: t, onViewChanged: o, onSelectionChange: a, options: s, editorDidMount: f }) {
const d = _(
(i) => {
t?.(i);
},
[t]
), u = _(
(i) => {
o?.(i);
},
[o]
), t = T(
(c) => {
r?.(c);
), c = _(
(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(X, {
value: e,
onChange: a,
onCreateEditor: t,
onUpdate: l,
extensions: ue
onChange: d,
onCreateEditor: u,
onUpdate: c,
extensions: le
}));
}
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 K(...e) {
return e.filter(Boolean).join(" ");
}
function ye({ view: e, pattern: o, active: r, getTime: n }) {
const s = I([]), i = I();
q(() => {
function ue({ view: e, pattern: t, active: o, getTime: a }) {
const s = H([]), f = H();
L(() => {
if (e)
if (o && r) {
let t = function() {
if (t && o) {
let u = 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(), m = [Math.max(f.current || c, c - 1 / 10, 0), c + 1 / 60];
f.current = m[1], s.current = s.current.filter((g) => g.whole.end > c);
const h = t.queryArc(...m).filter((g) => g.hasOnset());
s.current = s.current.concat(h), e.dispatch({ effects: z.of(s.current) });
} catch {
e.dispatch({ effects: O.of([]) });
e.dispatch({ effects: z.of([]) });
}
a = requestAnimationFrame(t);
}, a = requestAnimationFrame(t);
d = requestAnimationFrame(u);
}, d = requestAnimationFrame(u);
return () => {
cancelAnimationFrame(a);
cancelAnimationFrame(d);
};
} else
s.current = [], e.dispatch({ effects: O.of([]) });
}, [o, r, e]);
s.current = [], e.dispatch({ effects: z.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 fe = "_container_3i85k_1", me = "_header_3i85k_5", ge = "_buttons_3i85k_9", pe = "_button_3i85k_9", he = "_buttonDisabled_3i85k_17", be = "_error_3i85k_21", ve = "_body_3i85k_25", v = {
container: fe,
header: me,
buttons: ge,
button: pe,
buttonDisabled: he,
error: be,
body: ve
};
function W({ type: e }) {
return /* @__PURE__ */ y.createElement("svg", {
function O({ 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 Ee(e) {
return L(() => (window.addEventListener("message", e), () => window.removeEventListener("message", e)), [e]), _((t) => window.postMessage(t, "*"), []);
}
function we({
defaultOutput: e,
interval: t,
getTime: o,
evalOnMount: a = !1,
initialCode: s = "",
autolink: f = !1,
beforeEval: d,
afterEval: u,
onEvalError: c,
onToggle: i
}) {
const m = V(() => ye(), []), [h, g] = w(), [C, N] = w(), [p, y] = w(s), [M, S] = w(), [k, D] = w(), [F, x] = w(!1), b = p !== M, { scheduler: A, evaluate: T, start: J, stop: q, pause: Q } = V(
() => oe({
interval: t,
defaultOutput: e,
onSchedulerError: g,
onEvalError: (l) => {
N(l), c?.(l);
},
getTime: o,
transpiler: ne,
beforeEval: ({ code: l }) => {
y(l), d?.();
},
afterEval: ({ pattern: l, code: P }) => {
S(P), D(l), N(), g(), f && (window.location.hash = "#" + encodeURIComponent(btoa(P))), u?.();
},
onToggle: (l) => {
x(l), i?.(l);
}
}),
[e, t, o]
), W = Ee(({ data: { from: l, type: P } }) => {
P === "start" && l !== m && q();
}), R = _(
async (l = !0) => {
await T(p, l), W({ type: "start", from: m });
},
[T, p]
), I = H();
return L(() => {
!I.current && a && p && (I.current = !0, R());
}, [R, a, p]), L(() => () => {
A.stop();
}, [A]), {
code: p,
setCode: y,
error: h || C,
schedulerError: h,
scheduler: A,
evalError: C,
evaluate: T,
activateCode: R,
activeCode: M,
isDirty: b,
pattern: k,
started: F,
start: J,
stop: q,
pause: Q,
togglePlay: async () => {
F ? A.pause() : await R();
}
}, [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, {
};
}
function ye() {
return Math.floor((1 + Math.random()) * 65536).toString(16).substring(1);
}
const ke = () => re().currentTime;
function Se({ tune: e, hideOutsideView: t = !1, init: o, enableKeyboard: a }) {
const {
code: s,
setCode: f,
evaluate: d,
activateCode: u,
error: c,
isDirty: i,
activeCode: m,
pattern: h,
started: g,
scheduler: C,
togglePlay: N,
stop: p
} = we({
initialCode: e,
defaultOutput: te,
getTime: ke
}), [y, M] = w(), [S, k] = ee({
threshold: 0.01
}), D = H(), F = V(() => ((k || !t) && (D.current = !0), k || D.current), [k, t]);
return ue({
view: y,
pattern: h,
active: g && !m?.includes("strudel disable-highlighting"),
getTime: () => C.getPhase()
}), j(() => {
if (a) {
const x = async (b) => {
(b.ctrlKey || b.altKey) && (b.code === "Enter" ? (b.preventDefault(), ce(y), await u()) : b.code === "Period" && (p(), b.preventDefault()));
};
return window.addEventListener("keydown", x, !0), () => window.removeEventListener("keydown", x, !0);
}
}, [a, h, s, d, p, y]), /* @__PURE__ */ n.createElement("div", {
className: v.container,
ref: S
}, /* @__PURE__ */ n.createElement("div", {
className: v.header
}, /* @__PURE__ */ n.createElement("div", {
className: v.buttons
}, /* @__PURE__ */ n.createElement("button", {
className: K(v.button, g ? "sc-animate-pulse" : ""),
onClick: () => N()
}, /* @__PURE__ */ n.createElement(O, {
type: g ? "pause" : "play"
})), /* @__PURE__ */ n.createElement("button", {
className: K(i ? v.button : v.buttonDisabled),
onClick: () => u()
}, /* @__PURE__ */ n.createElement(O, {
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: v.error
}, c.message)), /* @__PURE__ */ n.createElement("div", {
className: v.body
}, F && /* @__PURE__ */ n.createElement(de, {
value: s,
onChange: f,
onViewChanged: M
})));
}
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) };
}
const Te = (e) => j(() => (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
Se as MiniRepl,
K as cx,
ce as flash,
ue as useHighlighting,
Te as useKeydown,
Ee as usePostMessage,
we as useStrudel
};
+1 -1
View File
@@ -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;
+6 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/react",
"version": "0.3.0",
"version": "0.4.1",
"description": "React components for strudel",
"main": "dist/index.cjs.js",
"module": "dist/index.es.js",
@@ -17,6 +17,7 @@
"scripts": {
"dev": "vite",
"build": "vite build",
"watch": "vite build --watch",
"preview": "vite preview"
},
"repository": {
@@ -38,9 +39,10 @@
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@codemirror/lang-javascript": "^6.1.1",
"@strudel.cycles/core": "^0.3.0",
"@strudel.cycles/eval": "^0.3.0",
"@strudel.cycles/tone": "^0.3.0",
"@strudel.cycles/core": "^0.4.0",
"@strudel.cycles/tone": "^0.4.0",
"@strudel.cycles/transpiler": "^0.4.0",
"@strudel.cycles/webaudio": "^0.4.1",
"@uiw/codemirror-themes": "^4.12.4",
"@uiw/react-codemirror": "^4.12.4",
"react-hook-inview": "^4.5.0"
+1 -1
View File
@@ -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'),
+33 -19
View File
@@ -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>
+1 -1
View File
@@ -2,7 +2,7 @@
background-color: transparent !important;
height: 100%;
z-index: 11;
font-size: 16px;
font-size: 18px;
}
.cm-theme-light {
-86
View File
@@ -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;
+2 -3
View File
@@ -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);
-150
View File
@@ -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;
+95 -14
View File
@@ -1,44 +1,125 @@
// import { Scheduler } from '@strudel.cycles/core';
import { useRef, useCallback, useEffect, useMemo, useState } from 'react';
import shapeshifter from '@strudel.cycles/eval/shapeshifter.mjs';
import { repl } from '@strudel.cycles/core/repl.mjs';
import { repl } from '@strudel.cycles/core';
import { transpiler } from '@strudel.cycles/transpiler';
import usePostMessage from './usePostMessage.mjs';
function useStrudel({ defaultOutput, interval, getTime, code, evalOnMount = false }) {
function useStrudel({
defaultOutput,
interval,
getTime,
evalOnMount = false,
initialCode = '',
autolink = false,
beforeEval,
afterEval,
onEvalError,
onToggle,
}) {
const id = useMemo(() => s4(), []);
// scheduler
const [schedulerError, setSchedulerError] = useState();
const [evalError, setEvalError] = useState();
const [activeCode, setActiveCode] = useState(code);
const [code, setCode] = useState(initialCode);
const [activeCode, setActiveCode] = useState();
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: shapeshifter,
onEval: ({ pattern: _pattern, code }) => {
transpiler,
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 broadcast = usePostMessage(({ data: { from, type } }) => {
if (type === 'start' && from !== id) {
// console.log('message', from, type);
stop();
}
});
const activateCode = useCallback(
async (autostart = true) => {
await evaluate(code, autostart);
broadcast({ type: 'start', from: id });
},
[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;
function s4() {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
}
-41
View File
@@ -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 };
}
-3
View File
@@ -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';
+16 -8
View File
@@ -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' },
],
+2 -1
View File
@@ -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',
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/serial",
"version": "0.1.3",
"version": "0.2.0",
"description": "Webserial API for strudel",
"main": "serial.mjs",
"repository": {
+13 -17
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/soundfonts",
"version": "0.3.0",
"version": "0.4.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/soundfonts",
"version": "0.3.0",
"version": "0.4.1",
"description": "Soundsfont support for strudel",
"main": "index.mjs",
"type": "module",
@@ -22,8 +22,8 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "^0.3.0",
"@strudel.cycles/webaudio": "^0.3.0",
"@strudel.cycles/core": "^0.4.0",
"@strudel.cycles/webaudio": "^0.4.1",
"sfumato": "^0.1.2",
"soundfont2": "^0.4.0"
},
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/tonal",
"version": "0.3.0",
"version": "0.4.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/tonal",
"version": "0.3.0",
"version": "0.4.0",
"description": "Tonal functions for strudel",
"main": "index.mjs",
"type": "module",
@@ -25,8 +25,9 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "^0.3.0",
"@strudel.cycles/core": "^0.4.0",
"@tonaljs/tonal": "^4.6.5",
"chord-voicings": "^0.0.1",
"webmidi": "^3.0.21"
}
}
+1 -1
View File
@@ -12,6 +12,6 @@ import { describe, it, expect } from 'vitest';
describe('tonal', () => {
it('Should run tonal functions ', () => {
expect(pure('c3').scale('C major').scaleTranspose(1)._firstCycleValues).toEqual(['D3']);
expect(pure('c3').scale('C major').scaleTranspose(1).firstCycleValues).toEqual(['D3']);
});
});
+3 -3
View File
@@ -75,7 +75,7 @@ function scaleOffset(scale, offset, note) {
*/
Pattern.prototype._transpose = function (intervalOrSemitones) {
return this._withHap((hap) => {
return this.withHap((hap) => {
const interval = !isNaN(Number(intervalOrSemitones))
? Interval.fromSemitones(intervalOrSemitones /* as number */)
: String(intervalOrSemitones);
@@ -111,7 +111,7 @@ Pattern.prototype._transpose = function (intervalOrSemitones) {
*/
Pattern.prototype._scaleTranspose = function (offset /* : number | string */) {
return this._withHap((hap) => {
return this.withHap((hap) => {
if (!hap.context.scale) {
throw new Error('can only use scaleTranspose after .scale');
}
@@ -142,7 +142,7 @@ Pattern.prototype._scaleTranspose = function (offset /* : number | string */) {
*/
Pattern.prototype._scale = function (scale /* : string */) {
return this._withHap((hap) => {
return this.withHap((hap) => {
let note = hap.value;
const asNumber = Number(note);
if (!isNaN(asNumber)) {
+1 -1
View File
@@ -51,7 +51,7 @@ Pattern.prototype.voicings = function (range) {
}
return this.fmapNested((event) => {
lastVoicing = getVoicing(event.value, lastVoicing, range);
return stack(...lastVoicing)._withContext(() => ({
return stack(...lastVoicing).withContext(() => ({
locations: event.context.locations || [],
}));
});
-4
View File
@@ -1,5 +1 @@
import './pianoroll.mjs';
export * from './tone.mjs';
export * from './draw.mjs';
export * from './ui.mjs';
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/tone",
"version": "0.3.0",
"version": "0.4.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
+2 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/tone",
"version": "0.3.0",
"version": "0.4.0",
"description": "Tone.js API for strudel",
"main": "index.mjs",
"type": "module",
@@ -22,8 +22,7 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "^0.3.0",
"chord-voicings": "^0.0.1",
"@strudel.cycles/core": "^0.4.0",
"tone": "^14.7.77"
}
}
+1 -1
View File
@@ -13,6 +13,6 @@ describe('tone', () => {
// const s = synth().chain(out()); // TODO: mock audio context?
// assert.deepStrictEqual(s, new Tone.Synth().chain(out()));
const s = {};
expect(pure('c3').tone(s)._firstCycleValues).toEqual(['c3']);
expect(pure('c3').tone(s).firstCycleValues).toEqual(['c3']);
});
});
+22 -25
View File
@@ -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);
}
});
};
+27
View File
@@ -0,0 +1,27 @@
# @strudel.cycles/transpiler
This package contains a JS code transpiler with the following features:
- add locations of mini notation strings (double quoted or backticked) for highlighting
- converts pseudo note variables to note strings
- adds return statement to the last expression
The transpiler is written with [acorn](https://github.com/acornjs/acorn) and aims to replace the `@strudel.cycles/eval` package, which uses [shift-ast](https://www.npmjs.com/package/shift-ast).
## Install
```sh
npm i @strudel.cycles/transpiler
```
## Use
```js
import { transpiler } from '@strudel.cycles/core';
import { evaluate } from '@strudel.cycles/core';
transpiler('note("c3 [e3,g3]")', { wrapAsync: false, addReturn: false, simpleLocs: true });
/* mini('c3 [e3,g3]').withMiniLocation(7,17) */
evaluate(note('c3 [e3,g3]'), transpiler); // returns pattern of above code
```
+5
View File
@@ -0,0 +1,5 @@
import { evaluate as _evaluate } from '@strudel.cycles/core';
import { transpiler } from './transpiler.mjs';
export * from './transpiler.mjs';
export const evaluate = (code) => _evaluate(code, transpiler);
+32
View File
@@ -0,0 +1,32 @@
{
"name": "@strudel.cycles/transpiler",
"version": "0.4.0",
"description": "Transpiler for strudel user code. Converts syntactically correct but semantically meaningless JS into evaluatable strudel code.",
"main": "index.mjs",
"scripts": {
"test": "vitest run"
},
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
},
"keywords": [
"tidalcycles",
"strudel",
"pattern",
"livecoding",
"algorave"
],
"author": "Felix Roos <flix91@gmail.com>",
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "^0.4.0",
"acorn": "^8.8.1",
"escodegen": "^2.0.0",
"estree-walker": "^3.0.1"
}
}
@@ -0,0 +1,39 @@
/*
transpiler.test.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/transpiler/test/transpiler.test.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 { describe, it, expect } from 'vitest';
import { transpiler } from '../transpiler.mjs';
const simple = { wrapAsync: false, addReturn: false, simpleLocs: true };
describe('transpiler', () => {
it('wraps double quote string with mini and adds location', () => {
expect(transpiler('"c3"', simple)).toEqual("mini('c3').withMiniLocation(0, 4);");
expect(transpiler('stack("c3","bd sd")', simple)).toEqual(
"stack(mini('c3').withMiniLocation(6, 10), mini('bd sd').withMiniLocation(11, 18));",
);
});
it('wraps backtick string with mini and adds location', () => {
expect(transpiler('`c3`', simple)).toEqual("mini('c3').withMiniLocation(0, 4);");
});
it('replaces note variables with note strings', () => {
expect(transpiler('seq(c3, d3)', simple)).toEqual("seq('c3', 'd3');");
});
it('keeps tagged template literal as is', () => {
expect(transpiler('xxx`c3`', simple)).toEqual('xxx`c3`;');
});
it('supports top level await', () => {
expect(transpiler("await samples('xxx');", simple)).toEqual("await samples('xxx');");
});
/* it('parses dynamic imports', () => {
expect(
transpiler("const { default: foo } = await import('https://bar.com/foo.js');", {
wrapAsync: false,
addReturn: false,
}),
).toEqual("const {default: foo} = await import('https://bar.com/foo.js');");
}); */
});
+129
View File
@@ -0,0 +1,129 @@
import escodegen from 'escodegen';
import { parse } from 'acorn';
import { walk } from 'estree-walker';
import { isNote } from '@strudel.cycles/core';
export function transpiler(input, options = {}) {
const { wrapAsync = false, addReturn = true, simpleLocs = false } = options;
let ast = parse(input, {
ecmaVersion: 2022,
allowAwaitOutsideFunction: true,
locations: true,
});
walk(ast, {
enter(node, parent, prop, index) {
if (isBackTickString(node, parent)) {
const { quasis, start, end } = node;
const { raw } = quasis[0].value;
this.skip();
return this.replace(miniWithLocation(raw, node, simpleLocs));
}
if (isStringWithDoubleQuotes(node)) {
const { value, start, end } = node;
this.skip();
return this.replace(miniWithLocation(value, node, simpleLocs));
}
if (node.type === 'Identifier' && isNote(node.name)) {
this.skip();
return this.replace({ type: 'Literal', value: node.name });
}
// TODO:
},
leave(node, parent, prop, index) {},
});
const { body } = ast;
if (!body?.[body.length - 1]?.expression) {
throw new Error('unexpected ast format without body expression');
}
// add return to last statement
if (addReturn) {
const { expression } = body[body.length - 1];
body[body.length - 1] = {
type: 'ReturnStatement',
argument: expression,
};
}
const output = escodegen.generate(ast);
if (wrapAsync) {
return `(async ()=>{${output}})()`;
}
return output;
}
function isStringWithDoubleQuotes(node, locations, code) {
const { raw, type } = node;
if (type !== 'Literal') {
return false;
}
return raw[0] === '"';
}
function isBackTickString(node, parent) {
return node.type === 'TemplateLiteral' && parent.type !== 'TaggedTemplateExpression';
}
function miniWithLocation(value, node, simpleLocs) {
let locs;
const { start: fromOffset, end: toOffset } = node;
if (simpleLocs) {
locs = [
{
type: 'Literal',
value: fromOffset,
},
{
type: 'Literal',
value: toOffset,
},
];
} else {
const {
loc: {
start: { line: fromLine, column: fromColumn },
end: { line: toLine, column: toColumn },
},
} = node;
locs = [
{
type: 'ArrayExpression',
elements: [fromLine, fromColumn, fromOffset].map((value) => ({
type: 'Literal',
value,
})),
},
{
type: 'ArrayExpression',
elements: [toLine, toColumn, toOffset].map((value) => ({
type: 'Literal',
value,
})),
},
];
}
// with location
return {
type: 'CallExpression',
callee: {
type: 'MemberExpression',
object: {
type: 'CallExpression',
callee: {
type: 'Identifier',
name: 'mini',
},
arguments: [{ type: 'Literal', value }],
optional: false,
},
property: {
type: 'Identifier',
name: 'withMiniLocation',
},
},
arguments: locs,
optional: false,
};
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/webaudio",
"version": "0.3.0",
"version": "0.4.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
+5 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/webaudio",
"version": "0.3.0",
"version": "0.4.1",
"description": "Web Audio helpers for Strudel",
"main": "index.mjs",
"type": "module",
@@ -8,7 +8,9 @@
"example": "examples"
},
"scripts": {
"example": "npx parcel examples/repl.html"
"example": "npx parcel examples/repl.html",
"build": "vite build",
"prepublish": "npm run build"
},
"repository": {
"type": "git",
@@ -28,6 +30,6 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "^0.3.0"
"@strudel.cycles/core": "^0.4.0"
}
}
+75 -60
View File
@@ -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`
+19
View File
@@ -0,0 +1,19 @@
import { defineConfig } from 'vite';
import { dependencies } from './package.json';
import { resolve } from 'path';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [],
build: {
lib: {
entry: resolve(__dirname, 'index.mjs'),
formats: ['es', 'cjs'],
fileName: (ext) => `index.${ext}.js`,
},
rollupOptions: {
external: [...Object.keys(dependencies)],
},
target: 'esnext',
},
});
+203 -250
View File
@@ -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;
@@ -196,7 +157,7 @@ function getDelay(orbit, delaytime, delayfeedback, t) {
if (!delays[orbit]) {
const ac = getAudioContext();
const dly = ac.createFeedbackDelay(1, delaytime, delayfeedback);
dly.start(t);
dly.start?.(t); // for some reason, this throws when audion extension is installed..
dly.connect(getDestination());
delays[orbit] = dly;
}
@@ -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);
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/webdirt",
"version": "0.3.0",
"version": "0.4.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/webdirt",
"version": "0.3.0",
"version": "0.4.0",
"description": "WebDirt integration for Strudel",
"main": "index.mjs",
"type": "module",
@@ -22,7 +22,7 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "^0.3.0",
"@strudel.cycles/core": "^0.4.0",
"WebDirt": "github:dktr0/WebDirt"
}
}
+27 -30
View File
@@ -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 });
}
});
};
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/xen",
"version": "0.3.0",
"version": "0.4.0",
"description": "Xenharmonic API for strudel",
"main": "index.mjs",
"scripts": {
@@ -24,6 +24,6 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "^0.3.0"
"@strudel.cycles/core": "^0.4.0"
}
}
+15
View File
@@ -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",
+4 -1
View File
@@ -3,12 +3,14 @@
"private": true,
"version": "0.0.0",
"scripts": {
"render-jsdoc": "cd ${PWD}/../tutorial/ && npm run render",
"predev": "npm run render-jsdoc",
"prebuild": "npm run render-jsdoc",
"dev": "vite --host",
"start": "vite",
"build": "vite build",
"preview": "vite preview",
"test": "vitest run --reporter verbose -v --no-isolate",
"snapshot": "vitest run -u --silent",
"add-license": "cat etc/agpl-header.txt ../docs/static/js/*LICENSE.txt > /tmp/strudel-license.txt && cp /tmp/strudel-license.txt ../docs/static/js/*LICENSE.txt",
"predeploy": "npm run build",
"deploy": "gh-pages -d ../docs",
@@ -16,6 +18,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",
+15
View File
@@ -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'; */
}
}
+215 -264
View File
@@ -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
View 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;
}

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