Compare commits

..

143 Commits

Author SHA1 Message Date
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 60ce34af43 Publish
- @strudel.cycles/core@0.3.0
 - @strudel.cycles/embed@0.2.0
 - @strudel.cycles/eval@0.3.0
 - @strudel.cycles/midi@0.3.0
 - @strudel.cycles/mini@0.3.0
 - @strudel.cycles/osc@0.2.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:12:57 +01:00
Felix Roos 2602ff6e34 hotfix: revert lerna upgrade 2022-11-06 19:10:09 +01:00
Felix Roos ef5cd36a37 Merge pull request #248 from tidalcycles/general-purpose-scheduler
General purpose scheduler
2022-11-06 19:03:19 +01:00
Felix Roos 7d47f5aba3 remove old clockworker + scheduler 2022-11-06 17:35:53 +01:00
Felix Roos 33cb506c71 add vanilla repl link to githack 2022-11-06 16:09:47 +01:00
Felix Roos 5fb36acb04 rebuild with correct base 2022-11-06 16:06:54 +01:00
Felix Roos 5f985f359b add vanilla repl build 2022-11-06 16:03:49 +01:00
Felix Roos 09045d5387 add vite vanilla js repl example 2022-11-06 15:50:53 +01:00
Felix Roos 371af755df move evaluate logic without transpiler to core
+ breaking change: evalScope is now imported from core
+ breaking change: deprecated extend is now removed
+ add repl.mjs
2022-11-06 12:12:43 +01:00
Felix Roos 0485632e22 Merge remote-tracking branch 'origin/main' into general-purpose-scheduler 2022-11-06 11:06:32 +01:00
Felix Roos c3d6de2412 Merge pull request #243 from tidalcycles/shared-tests
snapshot tests on shared snippets
2022-11-06 11:05:23 +01:00
Felix Roos 36d49b11d9 Merge pull request #247 from tidalcycles/some-tunes
added some tunes
2022-11-06 11:03:41 +01:00
Felix Roos 866277510e fix: samples function did not work in tests 2022-11-06 00:44:23 +01:00
Felix Roos da8e00eb4e Merge remote-tracking branch 'origin/main' into some-tunes 2022-11-06 00:38:05 +01:00
Felix Roos cd30b2af3f Merge pull request #246 from tidalcycles/readme-tlc
patchday
2022-11-06 00:37:11 +01:00
Felix Roos 38b3337d35 add amensister 2022-11-06 00:31:59 +01:00
Felix Roos 84d34aa12b update shift packages 2022-11-05 12:09:00 +01:00
Felix Roos 96b7b68900 add scheduled example 2022-11-05 11:34:15 +01:00
Felix Roos 2a6ffbf5a4 update lerna 2022-11-05 11:33:59 +01:00
Felix Roos 1ac99e260b update plugin-react + osc-js 2022-11-05 11:20:53 +01:00
Felix Roos 519062c8d0 update tailwind 2022-11-04 23:03:54 +01:00
Felix Roos 78d7cdf662 update more things 2022-11-04 21:05:24 +01:00
Felix Roos a191b1e811 update vite in repl 2022-11-04 21:01:04 +01:00
Felix Roos b372161874 update vite in react package 2022-11-04 20:58:36 +01:00
Felix Roos 5bbec7b17c package-lock 2022-11-04 20:46:06 +01:00
Felix Roos 54b847a8ef update tailwind + react 2022-11-04 20:45:45 +01:00
Felix Roos a40a0f730f update codemirror 2022-11-04 20:43:16 +01:00
Felix Roos a677b94297 update webmidi + remove tone piano 2022-11-04 20:36:33 +01:00
Felix Roos d102528d0a Merge pull request #244 from tidalcycles/readme-tlc
Readme + TLC
2022-11-04 20:28:22 +01:00
Felix Roos 854692f975 use only node 18 to save some energy 2022-11-04 20:25:31 +01:00
Felix Roos bdc321df4d update browserslist 2022-11-04 20:24:42 +01:00
Felix Roos 230e8e9d4d revert auto formatted snippet 2022-11-04 20:21:08 +01:00
Felix Roos 96c49689ec improve readmes 2022-11-04 20:15:46 +01:00
Felix Roos 698f560d56 export controls from core root 2022-11-04 20:15:36 +01:00
Felix Roos 67ee493e56 remove cross import + code format 2022-11-04 20:15:11 +01:00
Felix Roos 4fc7ef698a code format 2022-11-04 20:14:57 +01:00
Felix Roos 89434fc3bd replace old snippet 2022-11-04 20:14:47 +01:00
Felix Roos 6129f9afe8 fix: mini repl test tune 2022-11-04 20:13:25 +01:00
Felix Roos e5f38dbbc6 dbdump script 2022-11-04 17:30:19 +01:00
Felix Roos 5534db95bc exclude shared test for now 2022-11-03 19:01:23 +01:00
Felix Roos 37c45424fd all shared tests now pass 2022-11-03 18:57:27 +01:00
Felix Roos ed44f4ed0c add shared tunes test 2022-11-03 16:18:29 +01:00
Felix Roos f829f35cfa Merge pull request #242 from tidalcycles/example-tests
in source example tests
2022-11-03 15:16:03 +01:00
Felix Roos 3033acadbb remove log 2022-11-03 14:57:46 +01:00
Felix Roos 5c2a8a68c0 render doc.json before testing 2022-11-03 14:52:24 +01:00
Felix Roos e9544f8f92 test all in source examples 2022-11-03 14:47:27 +01:00
Felix Roos 1a6743dba9 hotfix: #241 2022-11-03 12:52:01 +01:00
Felix Roos 06d6621c14 Merge pull request #240 from tidalcycles/load-samples-from-url
feat: support github: links
2022-11-02 23:16:43 +01:00
Felix Roos d7b82edae3 feat: support github: links 2022-11-02 23:13:00 +01:00
Felix Roos 5eae156134 Merge pull request #239 from tidalcycles/load-samples-from-url
Load samples from url
2022-11-02 21:26:44 +01:00
Felix Roos d8843e6057 refactor prebake 2022-11-02 21:07:47 +01:00
Felix Roos cf7c734778 feat: can now load samples via url 2022-11-02 12:29:34 +01:00
Felix Roos 240a8930ef Merge pull request #238 from tidalcycles/object-arithmetic
Object arithmetic
2022-11-02 11:42:32 +01:00
Felix Roos 4f3046593a fix: test 2022-11-01 21:54:25 +01:00
Felix Roos a0bd5bdd35 simplify asNumber 2022-11-01 21:45:40 +01:00
Felix Roos e3bc63e4fa fix: osc notes 2022-11-01 21:45:25 +01:00
Felix Roos fbf7c6293f get rid of asNumber on arithmetic functions 2022-11-01 21:38:16 +01:00
Felix Roos e128b02da1 support object arithmetic 2022-11-01 21:31:39 +01:00
Felix Roos 2168dbc6fc flat rave 2022-11-01 17:09:18 +01:00
Felix Roos 97db895354 use new bank function 2022-11-01 00:38:38 +01:00
Felix Roos 48fc68bfdb Merge remote-tracking branch 'origin/tidal-drum-machines' into some-tunes 2022-11-01 00:36:53 +01:00
Felix Roos 3741ffbb59 Merge pull request #237 from tidalcycles/tidal-drum-machines
Tidal drum machines
2022-11-01 00:36:14 +01:00
Felix Roos caf58492f2 move sample data to json files 2022-10-31 23:58:12 +01:00
Felix Roos 0c5bdd0205 make bank a normal param 2022-10-31 23:30:50 +01:00
Felix Roos ae6da0d6ef Merge pull request #236 from tidalcycles/fix-mono
fx on stereo speakers
2022-10-31 21:39:23 +01:00
Felix Roos bdbb4cb495 add all drum machines + soundbank helper 2022-10-31 20:55:23 +01:00
Felix Roos 566c358e8d drum demo 2022-10-31 20:35:34 +01:00
Felix Roos 324cffe6df 808 + 909 2022-10-31 20:34:47 +01:00
Felix Roos dadb8119e8 fix: reverb mono 2022-10-31 20:34:07 +01:00
Felix Roos 335d54b5f4 fix: multichannel worklets 2022-10-31 20:33:42 +01:00
Felix Roos 0fd851999f little fluty tune 2022-10-30 20:38:12 +01:00
Felix Roos 50f88798d8 Merge remote-tracking branch 'origin/main' into some-tunes 2022-10-30 19:36:16 +01:00
Felix Roos 8033cfc49d add sample demo tune 2022-10-30 19:36:06 +01:00
Felix Roos 882f450ee8 Merge pull request #235 from tidalcycles/vcsl-samples
add vcsl sample library
2022-10-30 19:10:32 +01:00
Felix Roos 4d60a582fe note default to 36 2022-10-30 18:49:26 +01:00
Felix Roos 7ef3d33d0e add vcsl sample library 2022-10-30 15:52:51 +01:00
Alex McLean 24328ee8ae Fix zero length queries WIP (#234)
* failing tests for zero-width queries

* support zero width timespans in splitArcs. Fixes one test, breaks a load more..

* fix fastGap
2022-10-29 23:23:10 +01:00
Felix Roos d45c380389 add two tunes 2022-10-29 22:19:11 +02:00
Felix Roos fbb3d144a1 hotfix: missing note calls in some tunes 2022-10-29 18:07:46 +02:00
Felix Roos 7e7e299bce Merge remote-tracking branch 'origin/main' into general-purpose-scheduler 2022-10-29 17:56:30 +02:00
Felix Roos e715e2ba2f Merge remote-tracking branch 'origin/main' into general-purpose-scheduler 2022-09-25 21:16:37 +02:00
Felix Roos 63c6a9735c Merge branch 'main' into general-purpose-scheduler 2022-09-22 21:25:44 +02:00
Felix Roos 0c1529237c Merge commit '6dbb2cb98a41edc6ad6b3458a6f7067044cb66cd' into general-purpose-scheduler 2022-09-22 19:54:58 +02:00
Felix Roos 4f8a2a0900 another round of scheduling 2022-08-26 23:58:39 +02:00
Felix Roos 9a2a26e904 add a little dot 2022-08-23 21:56:38 +02:00
Felix Roos 067c3433b6 add nano-repl build 2022-08-23 21:55:06 +02:00
Felix Roos d0b07c4890 fix latency issues for larger intervals 2022-08-23 21:52:24 +02:00
Felix Roos 24f8f48a2f nano repl now supports highlighting 2022-08-23 21:24:25 +02:00
Felix Roos acafcc9888 build react pkg 2022-08-23 21:23:44 +02:00
Felix Roos ec4cfe6f86 use new getTime callback in repl + mini repl 2022-08-23 21:23:28 +02:00
Felix Roos 7b0c80c8be getTime callback for highlighting time 2022-08-23 21:23:01 +02:00
Felix Roos 29029714ee expose pattern form useStrudel 2022-08-23 21:22:28 +02:00
Felix Roos cd1203d1e2 fix pause + remove logs 2022-08-23 20:56:09 +02:00
Felix Roos 4dfc34515c fix nano-repl prod build 2022-08-23 18:23:36 +02:00
Felix Roos 11a9f3ced3 no top level await + no evaluate on mount 2022-08-23 18:04:35 +02:00
Felix Roos 5be1a7000c cleanup 2022-08-23 17:56:01 +02:00
Felix Roos 9059c1b850 scheduler is now finally tight 2022-08-23 17:55:10 +02:00
Felix Roos ec60fe42be fix scheduler again.. add comments 2022-08-23 11:30:00 +02:00
Felix Roos 275931969e scheduler error correction failed attempt 2022-08-22 22:15:32 +02:00
Felix Roos e37e0f189a build react package 2022-08-21 11:22:30 +02:00
Felix Roos 8103943396 wait for ctx.resume 2022-08-21 11:21:36 +02:00
Felix Roos 9497c99e9e merge evaluator and scheduler hooks + use getTime 2022-08-21 11:21:19 +02:00
Felix Roos 2172d1a767 scheduler error correction via getTime 2022-08-21 11:20:15 +02:00
Felix Roos e72c47869a comment out stuff that uses tone.js 2022-08-18 20:50:20 +02:00
Felix Roos 5ea2124e91 webaudio error handling 2022-08-18 20:46:42 +02:00
Felix Roos 0c08919bfa add codemirror + keyboard shortcuts 2022-08-18 16:16:23 +02:00
Felix Roos a7562c3803 add useKeydown helper 2022-08-18 16:15:44 +02:00
Felix Roos 5f4d8f6862 add tailwind 2022-08-18 16:15:23 +02:00
Felix Roos 9c7d0951b7 pause action + started flag 2022-08-18 16:13:50 +02:00
Felix Roos 75e57c75cf add master gain + panic 2022-08-18 16:13:27 +02:00
Felix Roos 04502b05d4 better default tune + fix dirty state 2022-08-18 15:16:29 +02:00
Felix Roos e3e3a8404a nano-repl add modules + proper eval 2022-08-18 00:27:16 +02:00
Felix Roos 48bf17614f fix: runtime error reify undefined 2022-08-18 00:26:57 +02:00
Felix Roos 1941b1ff65 add react nano-repl example 2022-08-17 23:59:40 +02:00
Felix Roos 46aa3d275a package-lock 2022-08-17 23:58:47 +02:00
Felix Roos d667e790b9 export new hooks 2022-08-17 23:58:34 +02:00
Felix Roos bc345b569b add hooks for scheduler and evaluator 2022-08-17 23:57:49 +02:00
Felix Roos ee84b4e013 export webaudio out as separate function 2022-08-17 23:56:36 +02:00
Felix Roos e77129fa51 scheduler error callback 2022-08-17 23:55:06 +02:00
Felix Roos 591855919e working default tune 2022-08-17 21:29:57 +02:00
Felix Roos 7af6188a8a move scheduler to core + move audioctx to userland 2022-08-17 21:26:00 +02:00
Felix Roos 1cd31cfce9 fix getFrequency for .note 2022-08-17 21:25:04 +02:00
129 changed files with 27143 additions and 6158 deletions
+1 -1
View File
@@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16, 17]
node-version: [18]
steps:
- uses: actions/checkout@v2
+3212 -3174
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -4,6 +4,7 @@
"private": true,
"description": "Port of tidalcycles to javascript",
"scripts": {
"pretest": "cd tutorial && npm run jsdoc-json",
"test": "vitest run --version",
"test-ui": "vitest --ui",
"test-coverage": "vitest --coverage",
@@ -43,7 +44,6 @@
"c8": "^7.12.0",
"events": "^3.3.0",
"gh-pages": "^4.0.0",
"happy-dom": "^6.0.4",
"jsdoc": "^3.6.10",
"jsdoc-json": "^2.0.2",
"jsdoc-to-markdown": "^7.1.1",
+1
View File
@@ -0,0 +1 @@
examples
+12 -5
View File
@@ -40,11 +40,6 @@ const generic_params = [
* @example
* n("0 1 2 3").s('east').osc()
*/
// TODO: nOut does not work
// TODO: notes don't work as expected
// current "workaround" for notes:
// s('superpiano').n("<c0 d0 e0 g0>"._asNumber()).osc()
// -> .n or .osc (or .superdirt) would need to convert note strings to numbers
// also see https://github.com/tidalcycles/strudel/pull/63
['f', 'n', 'The note or sample number to choose for a synth or sampleset'],
['f', 'note', 'The note or pitch to play a sound or synth with'],
@@ -100,6 +95,18 @@ const generic_params = [
'attack',
'a pattern of numbers to specify the attack time (in seconds) of an envelope applied to each sample.',
],
/**
* Select the sound bank to use. To be used together with `s`. The bank name (+ "_") will be prepended to the value of `s`.
*
* @name bank
* @param {string | Pattern} bank the name of the bank
* @example
* s("bd sd").bank('RolandTR909') // = s("RolandTR909_bd RolandTR909_sd")
*
*/
['f', 'bank', 'selects sound bank to use'],
// TODO: find out how this works?
/*
* Envelope decay time = the time it takes after the attack time to reach the sustain level.
+81
View File
@@ -0,0 +1,81 @@
/*
cyclist.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/cyclist.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 createClock from './zyklus.mjs';
export class Cyclist {
worker;
pattern;
started = false;
cps = 1; // TODO
getTime;
phase = 0;
constructor({ interval, onTrigger, onError, getTime, latency = 0.1 }) {
this.getTime = getTime;
const round = (x) => Math.round(x * 1000) / 1000;
this.clock = createClock(
getTime,
(phase, duration, tick) => {
if (tick === 0) {
this.origin = phase;
}
const begin = round(phase - this.origin);
this.phase = begin - latency;
const end = round(begin + duration);
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;
onTrigger?.(hap, deadline, duration);
}
});
} catch (e) {
console.warn('scheduler error', e);
onError?.(e);
}
}, // called slightly before each cycle
interval, // duration of each cycle
);
}
getPhase() {
return this.phase;
}
start() {
if (!this.pattern) {
throw new Error('Scheduler: no pattern set! call .setPattern first.');
}
this.clock.start();
this.started = true;
}
pause() {
this.clock.stop();
delete this.origin;
this.started = false;
}
stop() {
delete this.origin;
this.clock.stop();
this.started = false;
}
setPattern(pat, autostart = false) {
this.pattern = pat;
if (autostart && !this.started) {
this.start();
}
}
setCps(cps = 1) {
this.cps = cps;
}
log(begin, end, haps) {
const onsets = haps.filter((h) => h.hasOnset());
console.log(`${begin.toFixed(4)} - ${end.toFixed(4)} ${Array(onsets.length).fill('I').join('')}`);
}
}
+1
View File
@@ -21,6 +21,7 @@ import Fraction, { gcd } from './fraction.mjs';
* @example
* const line = drawLine("0 [1 2 3]", 10); // |0--123|0--123
* console.log(line);
* silence;
*/
function drawLine(pat, chars = 60) {
let cycle = 0;
+55
View File
@@ -0,0 +1,55 @@
/*
evaluate.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/eval/evaluate.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 * as strudel from '@strudel.cycles/core';
const { isPattern, Pattern } = strudel;
let scoped = false;
export const evalScope = async (...args) => {
if (scoped) {
console.warn('evalScope was called more than once.');
}
scoped = true;
const results = await Promise.allSettled(args);
const modules = results.filter((result) => result.status === 'fulfilled').map((r) => r.value);
results.forEach((result, i) => {
if (result.status === 'rejected') {
console.warn(`evalScope: module with index ${i} could not be loaded:`, result.reason);
}
});
Object.assign(globalThis, ...modules, Pattern.prototype.bootstrap());
};
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) => {
if (!scoped) {
await evalScope(); // at least scope Pattern.prototype.boostrap
}
if (transpiler) {
code = transpiler(code); // transform syntactically correct js code to semantically usable 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`;
throw new Error(message + (typeof evaluated === 'function' ? ', did you forget to call a function?' : '.'));
}
return { mode: 'javascript', pattern: evaluated };
};
+90
View File
@@ -0,0 +1,90 @@
<div style="position: absolute; bottom: 0; right: 0; padding: 4px; width: 100vw; display: flex">
<button id="start" style="font-size: 2em">start</button>
<button id="stop" style="font-size: 2em">stop</button>
<button id="slower" style="font-size: 2em">slower</button>
<button id="faster" style="font-size: 2em">faster</button>
</div>
<textarea
style="font-size: 2em; background: #052b49; color: #fff; height: 100%; width: 100%; outline: none; border: 0"
id="text"
spellcheck="false"
>
Loading...</textarea
>
<script type="module">
document.body.style = 'margin: 0';
import * as strudel from '@strudel.cycles/core';
const { cat, State, TimeSpan, Scheduler, getPlayableNoteValue, getFreq } = strudel;
Object.assign(window, strudel); // add strudel to eval scope
const ctx = new AudioContext();
const scheduler = new Scheduler({
// audioContext: getAudioContext(),
interval: 0.1,
onTrigger: (hap, time, duration) => {
const freq = getFrequency(hap);
const osc = ctx.createOscillator();
const gain = 0.2;
osc.frequency.value = freq;
osc.type = 'triangle';
const onset = ctx.currentTime + time;
const offset = onset + duration;
const attack = 0.05;
const release = 0.05;
osc.start(onset);
osc.stop(offset + release);
const g = ctx.createGain();
g.gain.setValueAtTime(gain, onset);
g.gain.linearRampToValueAtTime(gain, onset + attack);
g.gain.setValueAtTime(gain, offset - release);
g.gain.linearRampToValueAtTime(0, offset);
osc.connect(g);
g.connect(ctx.destination);
},
});
let initialCode = `stack('c4','e4',cat('g4','a4','b4','a4'))
.add(cat(0,1,2,3,4,3,2,1).slow(8))
.fast(2)
.cps(tri.range(1,8).slow(32))`;
try {
const base64 = decodeURIComponent(window.location.href.split('#')[1]);
initialCode = atob(base64);
} catch (err) {
console.warn('failed to decode', err);
}
const input = document.getElementById('text');
input.value = initialCode;
const evaluate = () => {
try {
const pattern = eval(input.value);
scheduler.setPattern(pattern);
window.location.hash = '#' + encodeURIComponent(btoa(input.value)); // update url hash
} catch (err) {
console.warn(err);
}
};
evaluate();
input.addEventListener('input', () => evaluate());
document.getElementById('start').addEventListener('click', async () => {
await ctx.resume();
scheduler.start();
});
document.getElementById('stop').addEventListener('click', () => scheduler.stop());
document.getElementById('slower').addEventListener('click', () => scheduler.setCps(scheduler.cps - 0.1));
document.getElementById('faster').addEventListener('click', () => scheduler.setCps(scheduler.cps + 0.1));
</script>
<!--
sequence(1,2).mul(55/2) // frequencies
.mul(slowcat(1,2))
.mul(slowcat(1,3/2,4/3,5/3).slow(8))
.fast(3)
.freq()
.velocity(.5)
.s('sawtooth')
.cutoff(800)
.out()
-->
+44
View File
@@ -0,0 +1,44 @@
<input
type="text"
id="text"
value="seq('c3','eb3','g3').note().s('sawtooth').out()"
style="width: 100%; font-size: 2em; outline: none; margin-bottom: 10px"
spellcheck="false"
/>
<button id="start">play</button>
<div id="output"></div>
<script type="module">
const strudel = await import('https://cdn.skypack.dev/@strudel.cycles/core@latest');
const controls = await import('https://cdn.skypack.dev/@strudel.cycles/core@latest/controls.mjs');
const { getAudioContext, Scheduler } = await import('https://cdn.skypack.dev/@strudel.cycles/webaudio@latest');
let scheduler;
const audioContext = getAudioContext();
const latency = 0.2;
Object.assign(window, strudel);
Object.assign(window, controls.default);
scheduler = new Scheduler({
audioContext,
interval: 0.1,
latency,
onEvent: (hap) => {
if (!hap.context.onTrigger) {
console.warn('no output chosen. use one of .out() .webdirt() .osc()');
}
},
});
let started;
document.getElementById('start').addEventListener('click', async () => {
const code = document.getElementById('text').value;
const pattern = eval(code);
const events = pattern._firstCycleValues;
console.log(code, '->', events);
scheduler.setPattern(pattern);
if (!started) {
scheduler.start();
started = true;
}
});
</script>
@@ -0,0 +1 @@
!dist
@@ -0,0 +1,10 @@
# vite-vanilla-repl
This folder demonstrates how to set up a strudel repl using vite and vanilla JS. Run it using:
```sh
npm i
npm run dev
```
or view it [live on githack](https://rawcdn.githack.com/tidalcycles/strudel/5fb36acb046ead7cd6ad3cd10f532e7f585f536a/packages/core/examples/vite-vanilla-repl/dist/index.html)
@@ -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
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};
@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite Vanilla Strudel REPL</title>
<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">
<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>
</body>
</html>
@@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite Vanilla Strudel REPL</title>
</head>
<body 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" src="./main.js"></script>
</body>
</html>
@@ -0,0 +1,29 @@
import { controls, repl, evalScope, setStringParser } from '@strudel.cycles/core';
import { mini } from '@strudel.cycles/mini';
import { getAudioContext, webaudioOutput } from '@strudel.cycles/webaudio';
// import { transpiler } from '@strudel.cycles/transpiler';
import tune from './tune.mjs';
const ctx = getAudioContext();
const input = document.getElementById('text');
input.innerHTML = tune;
evalScope(
controls,
import('@strudel.cycles/core'),
import('@strudel.cycles/mini'),
import('@strudel.cycles/webaudio'),
import('@strudel.cycles/tonal'),
);
setStringParser(mini)
const { evaluate } = repl({
defaultOutput: webaudioOutput,
getTime: () => ctx.currentTime,
// transpiler,
});
document.getElementById('start').addEventListener('click', () => {
ctx.resume();
evaluate(input.value);
});
+885
View File
@@ -0,0 +1,885 @@
{
"name": "vite-vanilla-repl",
"version": "0.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "vite-vanilla-repl",
"version": "0.0.0",
"devDependencies": {
"vite": "^3.2.0"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.13.tgz",
"integrity": "sha512-RY2fVI8O0iFUNvZirXaQ1vMvK0xhCcl0gqRj74Z6yEiO1zAUa7hbsdwZM1kzqbxHK7LFyMizipfXT3JME+12Hw==",
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.13.tgz",
"integrity": "sha512-+BoyIm4I8uJmH/QDIH0fu7MG0AEx9OXEDXnqptXCwKOlOqZiS4iraH1Nr7/ObLMokW3sOCeBNyD68ATcV9b9Ag==",
"cpu": [
"loong64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.13.tgz",
"integrity": "sha512-Cu3SC84oyzzhrK/YyN4iEVy2jZu5t2fz66HEOShHURcjSkOSAVL8C/gfUT+lDJxkVHpg8GZ10DD0rMHRPqMFaQ==",
"dev": true,
"hasInstallScript": true,
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=12"
},
"optionalDependencies": {
"@esbuild/android-arm": "0.15.13",
"@esbuild/linux-loong64": "0.15.13",
"esbuild-android-64": "0.15.13",
"esbuild-android-arm64": "0.15.13",
"esbuild-darwin-64": "0.15.13",
"esbuild-darwin-arm64": "0.15.13",
"esbuild-freebsd-64": "0.15.13",
"esbuild-freebsd-arm64": "0.15.13",
"esbuild-linux-32": "0.15.13",
"esbuild-linux-64": "0.15.13",
"esbuild-linux-arm": "0.15.13",
"esbuild-linux-arm64": "0.15.13",
"esbuild-linux-mips64le": "0.15.13",
"esbuild-linux-ppc64le": "0.15.13",
"esbuild-linux-riscv64": "0.15.13",
"esbuild-linux-s390x": "0.15.13",
"esbuild-netbsd-64": "0.15.13",
"esbuild-openbsd-64": "0.15.13",
"esbuild-sunos-64": "0.15.13",
"esbuild-windows-32": "0.15.13",
"esbuild-windows-64": "0.15.13",
"esbuild-windows-arm64": "0.15.13"
}
},
"node_modules/esbuild-android-64": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.13.tgz",
"integrity": "sha512-yRorukXBlokwTip+Sy4MYskLhJsO0Kn0/Fj43s1krVblfwP+hMD37a4Wmg139GEsMLl+vh8WXp2mq/cTA9J97g==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-android-arm64": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.13.tgz",
"integrity": "sha512-TKzyymLD6PiVeyYa4c5wdPw87BeAiTXNtK6amWUcXZxkV51gOk5u5qzmDaYSwiWeecSNHamFsaFjLoi32QR5/w==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-darwin-64": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.13.tgz",
"integrity": "sha512-WAx7c2DaOS6CrRcoYCgXgkXDliLnFv3pQLV6GeW1YcGEZq2Gnl8s9Pg7ahValZkpOa0iE/ojRVQ87sbUhF1Cbg==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-darwin-arm64": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.13.tgz",
"integrity": "sha512-U6jFsPfSSxC3V1CLiQqwvDuj3GGrtQNB3P3nNC3+q99EKf94UGpsG9l4CQ83zBs1NHrk1rtCSYT0+KfK5LsD8A==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-freebsd-64": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.13.tgz",
"integrity": "sha512-whItJgDiOXaDG/idy75qqevIpZjnReZkMGCgQaBWZuKHoElDJC1rh7MpoUgupMcdfOd+PgdEwNQW9DAE6i8wyA==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-freebsd-arm64": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.13.tgz",
"integrity": "sha512-6pCSWt8mLUbPtygv7cufV0sZLeylaMwS5Fznj6Rsx9G2AJJsAjQ9ifA+0rQEIg7DwJmi9it+WjzNTEAzzdoM3Q==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-linux-32": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.13.tgz",
"integrity": "sha512-VbZdWOEdrJiYApm2kkxoTOgsoCO1krBZ3quHdYk3g3ivWaMwNIVPIfEE0f0XQQ0u5pJtBsnk2/7OPiCFIPOe/w==",
"cpu": [
"ia32"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-linux-64": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.13.tgz",
"integrity": "sha512-rXmnArVNio6yANSqDQlIO4WiP+Cv7+9EuAHNnag7rByAqFVuRusLbGi2697A5dFPNXoO//IiogVwi3AdcfPC6A==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-linux-arm": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.13.tgz",
"integrity": "sha512-Ac6LpfmJO8WhCMQmO253xX2IU2B3wPDbl4IvR0hnqcPrdfCaUa2j/lLMGTjmQ4W5JsJIdHEdW12dG8lFS0MbxQ==",
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-linux-arm64": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.13.tgz",
"integrity": "sha512-alEMGU4Z+d17U7KQQw2IV8tQycO6T+rOrgW8OS22Ua25x6kHxoG6Ngry6Aq6uranC+pNWNMB6aHFPh7aTQdORQ==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-linux-mips64le": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.13.tgz",
"integrity": "sha512-47PgmyYEu+yN5rD/MbwS6DxP2FSGPo4Uxg5LwIdxTiyGC2XKwHhHyW7YYEDlSuXLQXEdTO7mYe8zQ74czP7W8A==",
"cpu": [
"mips64el"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-linux-ppc64le": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.13.tgz",
"integrity": "sha512-z6n28h2+PC1Ayle9DjKoBRcx/4cxHoOa2e689e2aDJSaKug3jXcQw7mM+GLg+9ydYoNzj8QxNL8ihOv/OnezhA==",
"cpu": [
"ppc64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-linux-riscv64": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.13.tgz",
"integrity": "sha512-+Lu4zuuXuQhgLUGyZloWCqTslcCAjMZH1k3Xc9MSEJEpEFdpsSU0sRDXAnk18FKOfEjhu4YMGaykx9xjtpA6ow==",
"cpu": [
"riscv64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-linux-s390x": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.13.tgz",
"integrity": "sha512-BMeXRljruf7J0TMxD5CIXS65y7puiZkAh+s4XFV9qy16SxOuMhxhVIXYLnbdfLrsYGFzx7U9mcdpFWkkvy/Uag==",
"cpu": [
"s390x"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-netbsd-64": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.13.tgz",
"integrity": "sha512-EHj9QZOTel581JPj7UO3xYbltFTYnHy+SIqJVq6yd3KkCrsHRbapiPb0Lx3EOOtybBEE9EyqbmfW1NlSDsSzvQ==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-openbsd-64": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.13.tgz",
"integrity": "sha512-nkuDlIjF/sfUhfx8SKq0+U+Fgx5K9JcPq1mUodnxI0x4kBdCv46rOGWbuJ6eof2n3wdoCLccOoJAbg9ba/bT2w==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-sunos-64": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.13.tgz",
"integrity": "sha512-jVeu2GfxZQ++6lRdY43CS0Tm/r4WuQQ0Pdsrxbw+aOrHQPHV0+LNOLnvbN28M7BSUGnJnHkHm2HozGgNGyeIRw==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-windows-32": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.13.tgz",
"integrity": "sha512-XoF2iBf0wnqo16SDq+aDGi/+QbaLFpkiRarPVssMh9KYbFNCqPLlGAWwDvxEVz+ywX6Si37J2AKm+AXq1kC0JA==",
"cpu": [
"ia32"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-windows-64": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.13.tgz",
"integrity": "sha512-Et6htEfGycjDrtqb2ng6nT+baesZPYQIW+HUEHK4D1ncggNrDNk3yoboYQ5KtiVrw/JaDMNttz8rrPubV/fvPQ==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-windows-arm64": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.13.tgz",
"integrity": "sha512-3bv7tqntThQC9SWLRouMDmZnlOukBhOCTlkzNqzGCmrkCJI7io5LLjwJBOVY6kOUlIvdxbooNZwjtBvj+7uuVg==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
"dev": true
},
"node_modules/has": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
"dev": true,
"dependencies": {
"function-bind": "^1.1.1"
},
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/is-core-module": {
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz",
"integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==",
"dev": true,
"dependencies": {
"has": "^1.0.3"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/nanoid": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz",
"integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==",
"dev": true,
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true
},
"node_modules/picocolors": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
"dev": true
},
"node_modules/postcss": {
"version": "8.4.18",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.18.tgz",
"integrity": "sha512-Wi8mWhncLJm11GATDaQKobXSNEYGUHeQLiQqDFG1qQ5UTDPTEvKw0Xt5NsTpktGTwLps3ByrWsBrG0rB8YQ9oA==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
}
],
"dependencies": {
"nanoid": "^3.3.4",
"picocolors": "^1.0.0",
"source-map-js": "^1.0.2"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/resolve": {
"version": "1.22.1",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
"integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==",
"dev": true,
"dependencies": {
"is-core-module": "^2.9.0",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
},
"bin": {
"resolve": "bin/resolve"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/rollup": {
"version": "2.79.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz",
"integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==",
"dev": true,
"bin": {
"rollup": "dist/bin/rollup"
},
"engines": {
"node": ">=10.0.0"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/source-map-js": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"dev": true,
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/vite": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/vite/-/vite-3.2.2.tgz",
"integrity": "sha512-pLrhatFFOWO9kS19bQ658CnRYzv0WLbsPih6R+iFeEEhDOuYgYCX2rztUViMz/uy/V8cLCJvLFeiOK7RJEzHcw==",
"dev": true,
"dependencies": {
"esbuild": "^0.15.9",
"postcss": "^8.4.18",
"resolve": "^1.22.1",
"rollup": "^2.79.1"
},
"bin": {
"vite": "bin/vite.js"
},
"engines": {
"node": "^14.18.0 || >=16.0.0"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
},
"peerDependencies": {
"less": "*",
"sass": "*",
"stylus": "*",
"sugarss": "*",
"terser": "^5.4.0"
},
"peerDependenciesMeta": {
"less": {
"optional": true
},
"sass": {
"optional": true
},
"stylus": {
"optional": true
},
"sugarss": {
"optional": true
},
"terser": {
"optional": true
}
}
}
},
"dependencies": {
"@esbuild/android-arm": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.13.tgz",
"integrity": "sha512-RY2fVI8O0iFUNvZirXaQ1vMvK0xhCcl0gqRj74Z6yEiO1zAUa7hbsdwZM1kzqbxHK7LFyMizipfXT3JME+12Hw==",
"dev": true,
"optional": true
},
"@esbuild/linux-loong64": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.13.tgz",
"integrity": "sha512-+BoyIm4I8uJmH/QDIH0fu7MG0AEx9OXEDXnqptXCwKOlOqZiS4iraH1Nr7/ObLMokW3sOCeBNyD68ATcV9b9Ag==",
"dev": true,
"optional": true
},
"esbuild": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.13.tgz",
"integrity": "sha512-Cu3SC84oyzzhrK/YyN4iEVy2jZu5t2fz66HEOShHURcjSkOSAVL8C/gfUT+lDJxkVHpg8GZ10DD0rMHRPqMFaQ==",
"dev": true,
"requires": {
"@esbuild/android-arm": "0.15.13",
"@esbuild/linux-loong64": "0.15.13",
"esbuild-android-64": "0.15.13",
"esbuild-android-arm64": "0.15.13",
"esbuild-darwin-64": "0.15.13",
"esbuild-darwin-arm64": "0.15.13",
"esbuild-freebsd-64": "0.15.13",
"esbuild-freebsd-arm64": "0.15.13",
"esbuild-linux-32": "0.15.13",
"esbuild-linux-64": "0.15.13",
"esbuild-linux-arm": "0.15.13",
"esbuild-linux-arm64": "0.15.13",
"esbuild-linux-mips64le": "0.15.13",
"esbuild-linux-ppc64le": "0.15.13",
"esbuild-linux-riscv64": "0.15.13",
"esbuild-linux-s390x": "0.15.13",
"esbuild-netbsd-64": "0.15.13",
"esbuild-openbsd-64": "0.15.13",
"esbuild-sunos-64": "0.15.13",
"esbuild-windows-32": "0.15.13",
"esbuild-windows-64": "0.15.13",
"esbuild-windows-arm64": "0.15.13"
}
},
"esbuild-android-64": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.13.tgz",
"integrity": "sha512-yRorukXBlokwTip+Sy4MYskLhJsO0Kn0/Fj43s1krVblfwP+hMD37a4Wmg139GEsMLl+vh8WXp2mq/cTA9J97g==",
"dev": true,
"optional": true
},
"esbuild-android-arm64": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.13.tgz",
"integrity": "sha512-TKzyymLD6PiVeyYa4c5wdPw87BeAiTXNtK6amWUcXZxkV51gOk5u5qzmDaYSwiWeecSNHamFsaFjLoi32QR5/w==",
"dev": true,
"optional": true
},
"esbuild-darwin-64": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.13.tgz",
"integrity": "sha512-WAx7c2DaOS6CrRcoYCgXgkXDliLnFv3pQLV6GeW1YcGEZq2Gnl8s9Pg7ahValZkpOa0iE/ojRVQ87sbUhF1Cbg==",
"dev": true,
"optional": true
},
"esbuild-darwin-arm64": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.13.tgz",
"integrity": "sha512-U6jFsPfSSxC3V1CLiQqwvDuj3GGrtQNB3P3nNC3+q99EKf94UGpsG9l4CQ83zBs1NHrk1rtCSYT0+KfK5LsD8A==",
"dev": true,
"optional": true
},
"esbuild-freebsd-64": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.13.tgz",
"integrity": "sha512-whItJgDiOXaDG/idy75qqevIpZjnReZkMGCgQaBWZuKHoElDJC1rh7MpoUgupMcdfOd+PgdEwNQW9DAE6i8wyA==",
"dev": true,
"optional": true
},
"esbuild-freebsd-arm64": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.13.tgz",
"integrity": "sha512-6pCSWt8mLUbPtygv7cufV0sZLeylaMwS5Fznj6Rsx9G2AJJsAjQ9ifA+0rQEIg7DwJmi9it+WjzNTEAzzdoM3Q==",
"dev": true,
"optional": true
},
"esbuild-linux-32": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.13.tgz",
"integrity": "sha512-VbZdWOEdrJiYApm2kkxoTOgsoCO1krBZ3quHdYk3g3ivWaMwNIVPIfEE0f0XQQ0u5pJtBsnk2/7OPiCFIPOe/w==",
"dev": true,
"optional": true
},
"esbuild-linux-64": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.13.tgz",
"integrity": "sha512-rXmnArVNio6yANSqDQlIO4WiP+Cv7+9EuAHNnag7rByAqFVuRusLbGi2697A5dFPNXoO//IiogVwi3AdcfPC6A==",
"dev": true,
"optional": true
},
"esbuild-linux-arm": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.13.tgz",
"integrity": "sha512-Ac6LpfmJO8WhCMQmO253xX2IU2B3wPDbl4IvR0hnqcPrdfCaUa2j/lLMGTjmQ4W5JsJIdHEdW12dG8lFS0MbxQ==",
"dev": true,
"optional": true
},
"esbuild-linux-arm64": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.13.tgz",
"integrity": "sha512-alEMGU4Z+d17U7KQQw2IV8tQycO6T+rOrgW8OS22Ua25x6kHxoG6Ngry6Aq6uranC+pNWNMB6aHFPh7aTQdORQ==",
"dev": true,
"optional": true
},
"esbuild-linux-mips64le": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.13.tgz",
"integrity": "sha512-47PgmyYEu+yN5rD/MbwS6DxP2FSGPo4Uxg5LwIdxTiyGC2XKwHhHyW7YYEDlSuXLQXEdTO7mYe8zQ74czP7W8A==",
"dev": true,
"optional": true
},
"esbuild-linux-ppc64le": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.13.tgz",
"integrity": "sha512-z6n28h2+PC1Ayle9DjKoBRcx/4cxHoOa2e689e2aDJSaKug3jXcQw7mM+GLg+9ydYoNzj8QxNL8ihOv/OnezhA==",
"dev": true,
"optional": true
},
"esbuild-linux-riscv64": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.13.tgz",
"integrity": "sha512-+Lu4zuuXuQhgLUGyZloWCqTslcCAjMZH1k3Xc9MSEJEpEFdpsSU0sRDXAnk18FKOfEjhu4YMGaykx9xjtpA6ow==",
"dev": true,
"optional": true
},
"esbuild-linux-s390x": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.13.tgz",
"integrity": "sha512-BMeXRljruf7J0TMxD5CIXS65y7puiZkAh+s4XFV9qy16SxOuMhxhVIXYLnbdfLrsYGFzx7U9mcdpFWkkvy/Uag==",
"dev": true,
"optional": true
},
"esbuild-netbsd-64": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.13.tgz",
"integrity": "sha512-EHj9QZOTel581JPj7UO3xYbltFTYnHy+SIqJVq6yd3KkCrsHRbapiPb0Lx3EOOtybBEE9EyqbmfW1NlSDsSzvQ==",
"dev": true,
"optional": true
},
"esbuild-openbsd-64": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.13.tgz",
"integrity": "sha512-nkuDlIjF/sfUhfx8SKq0+U+Fgx5K9JcPq1mUodnxI0x4kBdCv46rOGWbuJ6eof2n3wdoCLccOoJAbg9ba/bT2w==",
"dev": true,
"optional": true
},
"esbuild-sunos-64": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.13.tgz",
"integrity": "sha512-jVeu2GfxZQ++6lRdY43CS0Tm/r4WuQQ0Pdsrxbw+aOrHQPHV0+LNOLnvbN28M7BSUGnJnHkHm2HozGgNGyeIRw==",
"dev": true,
"optional": true
},
"esbuild-windows-32": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.13.tgz",
"integrity": "sha512-XoF2iBf0wnqo16SDq+aDGi/+QbaLFpkiRarPVssMh9KYbFNCqPLlGAWwDvxEVz+ywX6Si37J2AKm+AXq1kC0JA==",
"dev": true,
"optional": true
},
"esbuild-windows-64": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.13.tgz",
"integrity": "sha512-Et6htEfGycjDrtqb2ng6nT+baesZPYQIW+HUEHK4D1ncggNrDNk3yoboYQ5KtiVrw/JaDMNttz8rrPubV/fvPQ==",
"dev": true,
"optional": true
},
"esbuild-windows-arm64": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.13.tgz",
"integrity": "sha512-3bv7tqntThQC9SWLRouMDmZnlOukBhOCTlkzNqzGCmrkCJI7io5LLjwJBOVY6kOUlIvdxbooNZwjtBvj+7uuVg==",
"dev": true,
"optional": true
},
"fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"optional": true
},
"function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
"dev": true
},
"has": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
"dev": true,
"requires": {
"function-bind": "^1.1.1"
}
},
"is-core-module": {
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz",
"integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==",
"dev": true,
"requires": {
"has": "^1.0.3"
}
},
"nanoid": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz",
"integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==",
"dev": true
},
"path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true
},
"picocolors": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
"dev": true
},
"postcss": {
"version": "8.4.18",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.18.tgz",
"integrity": "sha512-Wi8mWhncLJm11GATDaQKobXSNEYGUHeQLiQqDFG1qQ5UTDPTEvKw0Xt5NsTpktGTwLps3ByrWsBrG0rB8YQ9oA==",
"dev": true,
"requires": {
"nanoid": "^3.3.4",
"picocolors": "^1.0.0",
"source-map-js": "^1.0.2"
}
},
"resolve": {
"version": "1.22.1",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
"integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==",
"dev": true,
"requires": {
"is-core-module": "^2.9.0",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
}
},
"rollup": {
"version": "2.79.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz",
"integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==",
"dev": true,
"requires": {
"fsevents": "~2.3.2"
}
},
"source-map-js": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
"dev": true
},
"supports-preserve-symlinks-flag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"dev": true
},
"vite": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/vite/-/vite-3.2.2.tgz",
"integrity": "sha512-pLrhatFFOWO9kS19bQ658CnRYzv0WLbsPih6R+iFeEEhDOuYgYCX2rztUViMz/uy/V8cLCJvLFeiOK7RJEzHcw==",
"dev": true,
"requires": {
"esbuild": "^0.15.9",
"fsevents": "~2.3.2",
"postcss": "^8.4.18",
"resolve": "^1.22.1",
"rollup": "^2.79.1"
}
}
}
}
@@ -0,0 +1,15 @@
{
"name": "vite-vanilla-repl",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"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"
}
}
@@ -0,0 +1,39 @@
/* export default `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)>")`;
*/
export default `stack(
n("c3 [eb3,g3]")
.delay("<0 .5>")
.delaytime(".16 | .33")
.delayfeedback(".6 | .8")
).sometimes(x=>x.speed("-1"))`;
+5 -2
View File
@@ -4,10 +4,10 @@ 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/>.
*/
export * from './controls.mjs';
import controls from './controls.mjs';
export * from './euclid.mjs';
import Fraction from './fraction.mjs';
export { Fraction };
export { Fraction, controls };
export * from './hap.mjs';
export * from './pattern.mjs';
export * from './signal.mjs';
@@ -15,6 +15,9 @@ export * from './state.mjs';
export * from './timespan.mjs';
export * from './util.mjs';
export * from './speak.mjs';
export * from './evaluate.mjs';
export * from './repl.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';
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/core",
"version": "0.2.0",
"version": "0.3.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/core",
"version": "0.2.0",
"version": "0.3.2",
"description": "Port of Tidal Cycles to JavaScript",
"main": "index.mjs",
"type": "module",
+53 -53
View File
@@ -10,9 +10,15 @@ import Hap from './hap.mjs';
import State from './state.mjs';
import { unionWithObj } from './value.mjs';
import { isNote, toMidi, compose, removeUndefineds, flatten, id, listRange, curry, mod } from './util.mjs';
import { compose, removeUndefineds, flatten, id, listRange, curry, mod, numeralArgs, parseNumeral } from './util.mjs';
import drawLine from './drawLine.mjs';
let stringParser;
// parser is expected to turn a string into a pattern
// if set, the reify function will parse all strings with it
// intended to use with mini to automatically interpret all strings as mini notation
export const setStringParser = (parser) => (stringParser = parser);
/** @class Class representing a pattern. */
export class Pattern {
_Pattern = true; // this property is used to detect if a pattern that fails instanceof Pattern is an instance of another Pattern
@@ -32,8 +38,10 @@ export class Pattern {
* @param {Fraction | number} end to time
* @returns Hap[]
* @example
* const pattern = sequence('a', ['b', 'c']);
* const haps = pattern.queryArc(0, 1);
* const pattern = sequence('a', ['b', 'c'])
* const haps = pattern.queryArc(0, 1)
* console.log(haps)
* silence
*/
queryArc(begin, end) {
return this.query(new State(new TimeSpan(begin, end)));
@@ -63,6 +71,17 @@ export class Pattern {
return new Pattern((state) => this.query(state.withSpan(func)));
}
withQuerySpanMaybe(func) {
const pat = this;
return new Pattern((state) => {
const newState = state.withSpan(func);
if (!newState.span) {
return [];
}
return pat.query(newState);
});
}
/**
* As with {@link Pattern#withQuerySpan}, but the function is applied to both the
* begin and end time of the query timespan.
@@ -435,34 +454,8 @@ export class Pattern {
return otherPat.fmap((b) => this.fmap((a) => func(a)(b)))._TrigzeroJoin();
}
_asNumber(dropfails = false, softfail = false) {
return this._withHap((hap) => {
const asNumber = Number(hap.value);
if (!isNaN(asNumber)) {
return hap.withValue(() => asNumber);
}
const specialValue = {
e: Math.E,
pi: Math.PI,
}[hap.value];
if (typeof specialValue !== 'undefined') {
return hap.withValue(() => specialValue);
}
if (isNote(hap.value)) {
// set context type to midi to let the player know its meant as midi number and not as frequency
return new Hap(hap.whole, hap.part, toMidi(hap.value), { ...hap.context, type: 'midi' });
}
if (dropfails) {
// return 'nothing'
return undefined;
}
if (softfail) {
// return original hap
return hap;
}
throw new Error('cannot parse as number: "' + hap.value + '"');
return hap;
});
_asNumber() {
return this.fmap(parseNumeral);
}
/**
@@ -744,12 +737,17 @@ export class Pattern {
// // there is no gap.. so maybe revert to _fast?
// return this._fast(factor)
// }
// A bit fiddly, to drop zero-width queries at the start of the next cycle
const qf = function (span) {
const cycle = span.begin.sam();
const begin = cycle.add(span.begin.sub(cycle).mul(factor).min(1));
const end = cycle.add(span.end.sub(cycle).mul(factor).min(1));
return new TimeSpan(begin, end);
const bpos = span.begin.sub(cycle).mul(factor).min(1);
const epos = span.end.sub(cycle).mul(factor).min(1);
if (bpos >= 1) {
return undefined;
}
return new TimeSpan(cycle.add(bpos), cycle.add(epos));
};
// Also fiddly, to maintain the right 'whole' relative to the part
const ef = function (hap) {
const begin = hap.part.begin;
const end = hap.part.end;
@@ -765,7 +763,7 @@ export class Pattern {
);
return new Hap(newWhole, newPart, hap.value, hap.context);
};
return this.withQuerySpan(qf)._withHap(ef)._splitQueries();
return this.withQuerySpanMaybe(qf)._withHap(ef)._splitQueries();
}
// Compress each cycle into the given timespan, leaving a gap
@@ -1287,7 +1285,7 @@ export class Pattern {
}
// sets absolute duration of haps
/* _duration(value) {
_duration(value) {
return this.withHapSpan((span) => new TimeSpan(span.begin, span.begin.add(value)));
}
@@ -1301,7 +1299,7 @@ export class Pattern {
*/
_legato(value) {
return this.withHapSpan((span) => new TimeSpan(span.begin, span.begin.add(span.end.sub(span.begin).mul(value))));
}
}
/**
*
@@ -1315,7 +1313,6 @@ export class Pattern {
_velocity(velocity) {
return this._withContext((context) => ({ ...context, velocity: (context.velocity || 1) * velocity }));
}
*/
/**
* Makes the sample fit the given number of cycles by changing the speed.
@@ -1371,9 +1368,6 @@ function _composeOp(a, b, func) {
// Make composers
(function () {
const num = (pat) => pat._asNumber();
const numOrString = (pat) => pat._asNumber(false, true);
// pattern composers
const composers = {
set: [(a, b) => b],
@@ -1397,7 +1391,7 @@ function _composeOp(a, b, func) {
* // Behind the scenes, the notes are converted to midi numbers:
* // "48 52 55".add("<0 5 7 0>").note()
*/
add: [(a, b) => a + b, numOrString], // support string concatenation
add: [numeralArgs((a, b) => a + b)], // support string concatenation
/**
*
* Like add, but the given numbers are subtracted.
@@ -1407,7 +1401,7 @@ function _composeOp(a, b, func) {
* "0 2 4".sub("<0 1 2 3>").scale('C4 minor').note()
* // See add for more information.
*/
sub: [(a, b) => a - b, num],
sub: [numeralArgs((a, b) => a - b)],
/**
*
* Multiplies each number by the given factor.
@@ -1416,21 +1410,21 @@ function _composeOp(a, b, func) {
* @example
* "1 1.5 [1.66, <2 2.33>]".mul(150).freq()
*/
mul: [(a, b) => a * b, num],
mul: [numeralArgs((a, b) => a * b)],
/**
*
* Divides each number by the given factor.
* @name div
* @memberof Pattern
*/
div: [(a, b) => a / b, num],
mod: [mod, num],
pow: [Math.pow, num],
_and: [(a, b) => a & b, num],
_or: [(a, b) => a | b, num],
_xor: [(a, b) => a ^ b, num],
_lshift: [(a, b) => a << b, num],
_rshift: [(a, b) => a >> b, num],
div: [numeralArgs((a, b) => a / b)],
mod: [numeralArgs(mod)],
pow: [numeralArgs(Math.pow)],
_and: [numeralArgs((a, b) => a & b)],
_or: [numeralArgs((a, b) => a | b)],
_xor: [numeralArgs((a, b) => a ^ b)],
_lshift: [numeralArgs((a, b) => a << b)],
_rshift: [numeralArgs((a, b) => a >> b)],
// TODO - force numerical comparison if both look like numbers?
lt: [(a, b) => a < b],
@@ -1502,15 +1496,18 @@ Pattern.prototype.patternified = [
'chop',
'color',
'cpm',
'duration',
'early',
'fast',
'jux',
'late',
'legato',
'linger',
'ply',
'segment',
'striate',
'slow',
'velocity',
];
// aliases
@@ -1554,7 +1551,7 @@ export function pure(value) {
export function isPattern(thing) {
// thing?.constructor?.name !== 'Pattern' // <- this will fail when code is mangled
const is = thing instanceof Pattern || thing._Pattern;
const is = thing instanceof Pattern || thing?._Pattern;
if (!thing instanceof Pattern) {
console.warn(
`Found Pattern that fails "instanceof Pattern" check.
@@ -1570,6 +1567,9 @@ export function reify(thing) {
if (isPattern(thing)) {
return thing;
}
if (stringParser && typeof thing === 'string') {
return stringParser(thing);
}
return pure(thing);
}
+23
View File
@@ -0,0 +1,23 @@
import { Cyclist } from './cyclist.mjs';
import { evaluate as _evaluate } from './evaluate.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) => {
if (!code) {
throw new Error('no code to evaluate');
}
try {
const { pattern } = await _evaluate(code, transpiler);
scheduler.setPattern(pattern, true);
onEval?.({
pattern,
code,
});
} catch (err) {
console.warn(`eval error: ${err.message}`);
onEvalError?.(err);
}
};
return { scheduler, evaluate };
}
+2 -1
View File
@@ -238,6 +238,7 @@ export const wchoose = (...pairs) => wchooseWith(rand, ...pairs);
export const wchooseCycles = (...pairs) => _wchooseWith(rand, ...pairs).innerJoin();
// this function expects pat to be a pattern of floats...
export const perlinWith = (pat) => {
const pata = pat.fmap(Math.floor);
const patb = pat.fmap((t) => Math.floor(t) + 1);
@@ -255,7 +256,7 @@ export const perlinWith = (pat) => {
* s("bd sd,hh*4").cutoff(perlin.range(500,2000))
*
*/
export const perlin = perlinWith(time);
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));
+29 -10
View File
@@ -47,6 +47,9 @@ import {
import { steady } from '../signal.mjs';
import controls from '../controls.mjs';
const { n } = 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);
@@ -137,6 +140,9 @@ describe('Pattern', () => {
it('Can make a pattern', () => {
expect(pure('hello').query(st(0.5, 2.5)).length).toBe(3);
});
it('Supports zero-width queries', () => {
expect(pure('hello').queryArc(0, 0).length).toBe(1);
});
});
describe('fmap()', () => {
it('Can add things', () => {
@@ -191,6 +197,9 @@ describe('Pattern', () => {
sequence([11, [12, 13]], [21, [22, 23]], [31, [32, 33]]),
);
});
it('can add object patterns', () => {
sameFirst(n(sequence(1, [2, 3])).add(n(10)), n(sequence(11, [12, 13])));
});
});
describe('keep()', () => {
it('can structure In()', () => {
@@ -373,9 +382,10 @@ describe('Pattern', () => {
);
});
it('copes with breaking up events across cycles', () => {
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')]
);
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'),
]);
});
});
describe('_compressSpan()', () => {
@@ -430,6 +440,16 @@ describe('Pattern', () => {
// mini('[c3 g3]/2 eb3') always plays [c3 eb3]
// mini('eb3 [c3 g3]/2 ') always plays [c3 g3]
});
it('Supports zero-length queries', () => {
expect(steady('a')._slow(1).queryArc(0, 0)).toStrictEqual(steady('a').queryArc(0, 0));
});
});
describe('slow()', () => {
it('Supports zero-length queries', () => {
expect(steady('a').slow(1)._setContext({}).queryArc(0, 0)).toStrictEqual(
steady('a')._setContext({}).queryArc(0, 0),
);
});
});
describe('inside', () => {
it('can rev inside a cycle', () => {
@@ -799,10 +819,11 @@ describe('Pattern', () => {
});
it('Squeezes to the correct cycle', () => {
expect(
pure(time.struct(true))._squeezeJoin().queryArc(3,4).map(x => x.value)
).toStrictEqual(
[Fraction(3.5)]
)
pure(time.struct(true))
._squeezeJoin()
.queryArc(3, 4)
.map((x) => x.value),
).toStrictEqual([Fraction(3.5)]);
});
});
describe('ply', () => {
@@ -855,9 +876,7 @@ describe('Pattern', () => {
});
describe('range', () => {
it('Can be patterned', () => {
expect(sequence(0, 0).range(sequence(0, 0.5), 1).firstCycle()).toStrictEqual(
sequence(0, 0.5).firstCycle(),
);
expect(sequence(0, 0).range(sequence(0, 0.5), 1).firstCycle()).toStrictEqual(sequence(0, 0.5).firstCycle());
});
});
describe('range2', () => {
+80 -17
View File
@@ -5,7 +5,20 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import { pure } from '../pattern.mjs';
import { isNote, tokenizeNote, toMidi, fromMidi, mod, compose, getFrequency, getPlayableNoteValue } from '../util.mjs';
import {
isNote,
tokenizeNote,
toMidi,
fromMidi,
mod,
compose,
getFrequency,
getPlayableNoteValue,
parseNumeral,
parseFractional,
numeralArgs,
fractionalArgs,
} from '../util.mjs';
import { describe, it, expect } from 'vitest';
describe('isNote', () => {
@@ -92,16 +105,16 @@ describe('getFrequency', () => {
expect(getFrequency(happify(57, { type: 'midi' }))).toEqual(220);
});
it('should return frequencies unchanged', () => {
expect(getFrequency(happify(440, { type: 'frequency' }))).toEqual(440);
expect(getFrequency(happify(440, { type: 'frequency' }))).toEqual(440);
expect(getFrequency(happify(432, { type: 'frequency' }))).toEqual(432);
});
it('should turn object with a "freq" property into frequency', () => {
expect(getFrequency(happify({freq: 220}))).toEqual(220)
expect(getFrequency(happify({freq: 440}))).toEqual(440)
expect(getFrequency(happify({ freq: 220 }))).toEqual(220);
expect(getFrequency(happify({ freq: 440 }))).toEqual(440);
});
it('should throw an error when given a non-note', () => {
expect(() => getFrequency(happify('Q'))).toThrowError(`not a note or frequency: Q`)
expect(() => getFrequency(happify('Z'))).toThrowError(`not a note or frequency: Z`)
expect(() => getFrequency(happify('Q'))).toThrowError(`not a note or frequency: Q`);
expect(() => getFrequency(happify('Z'))).toThrowError(`not a note or frequency: Z`);
});
});
@@ -140,22 +153,72 @@ describe('compose', () => {
describe('getPlayableNoteValue', () => {
const happify = (val, context = {}) => pure(val).firstCycle()[0].setContext(context);
it('should return object "note" property', () => {
expect(getPlayableNoteValue(happify({note: "a4"}))).toEqual('a4')
expect(getPlayableNoteValue(happify({ note: 'a4' }))).toEqual('a4');
});
it('should return object "n" property', () => {
expect(getPlayableNoteValue(happify({n: "a4"}))).toEqual('a4')
expect(getPlayableNoteValue(happify({ n: 'a4' }))).toEqual('a4');
});
it('should return object "value" property', () => {
expect(getPlayableNoteValue(happify({value: "a4"}))).toEqual('a4')
expect(getPlayableNoteValue(happify({ value: 'a4' }))).toEqual('a4');
});
it('should turn midi into frequency', () => {
expect(getPlayableNoteValue(happify(57, {type: 'midi'}))).toEqual(220)
})
expect(getPlayableNoteValue(happify(57, { type: 'midi' }))).toEqual(220);
});
it('should return frequency value', () => {
expect(getPlayableNoteValue(happify(220, {type: 'frequency'}))).toEqual(220)
})
expect(getPlayableNoteValue(happify(220, { type: 'frequency' }))).toEqual(220);
});
it('should throw an error if value is not an object, number, or string', () => {
expect(() => getPlayableNoteValue(happify(false))).toThrowError(`not a note: false`)
expect(() => getPlayableNoteValue(happify(undefined))).toThrowError(`not a note: undefined`)
})
});
expect(() => getPlayableNoteValue(happify(false))).toThrowError(`not a note: false`);
expect(() => getPlayableNoteValue(happify(undefined))).toThrowError(`not a note: undefined`);
});
});
describe('parseNumeral', () => {
it('should parse numbers as is', () => {
expect(parseNumeral(4)).toBe(4);
expect(parseNumeral(0)).toBe(0);
expect(parseNumeral(20)).toBe(20);
expect(parseNumeral('20')).toBe(20);
expect(parseNumeral(1.5)).toBe(1.5);
});
it('should parse notes', () => {
expect(parseNumeral('c4')).toBe(60);
expect(parseNumeral('c#4')).toBe(61);
expect(parseNumeral('db4')).toBe(61);
});
it('should throw an error for unknown strings', () => {
expect(() => parseNumeral('xyz')).toThrowError('cannot parse as numeral: "xyz"');
});
});
describe('parseFractional', () => {
it('should parse numbers as is', () => {
expect(parseFractional(4)).toBe(4);
expect(parseFractional(0)).toBe(0);
expect(parseFractional(20)).toBe(20);
expect(parseFractional('20')).toBe(20);
expect(parseFractional(1.5)).toBe(1.5);
});
it('should parse fractional shorthands values', () => {
expect(parseFractional('w')).toBe(1);
expect(parseFractional('h')).toBe(0.5);
expect(parseFractional('q')).toBe(0.25);
expect(parseFractional('e')).toBe(0.125);
});
it('should throw an error for unknown strings', () => {
expect(() => parseFractional('xyz')).toThrowError('cannot parse as fractional: "xyz"');
});
});
describe('numeralArgs', () => {
it('should convert function arguments to numbers', () => {
const add = numeralArgs((a, b) => a + b);
expect(add('c4', 2)).toBe(62);
});
});
describe('fractionalArgs', () => {
it('should convert function arguments to numbers', () => {
const add = fractionalArgs((a, b) => a + b);
expect(add('q', 2)).toBe(2.25);
});
});
+5
View File
@@ -18,6 +18,11 @@ export class TimeSpan {
const end = this.end;
const end_sam = end.sam();
// Support zero-width timespans
if (begin.equals(end)) {
return([new TimeSpan(begin, end)]);
}
while (end.gt(begin)) {
// If begin and end are in the same cycle, we're done.
if (begin.sam().equals(end_sam)) {
+45 -6
View File
@@ -73,8 +73,11 @@ export const getPlayableNoteValue = (hap) => {
export const getFrequency = (hap) => {
let { value, context } = hap;
// if value is number => interpret as midi number as long as its not marked as frequency
if (typeof value === 'object' && value.freq) {
return value.freq;
if (typeof value === 'object') {
if (value.freq) {
return value.freq;
}
return getFreq(value.note || value.n || value.value);
}
if (typeof value === 'number' && context.type !== 'frequency') {
value = fromMidi(hap.value);
@@ -131,9 +134,45 @@ export function curry(func, overload) {
return fn;
}
export function objectify(value) {
if (typeof value === 'object' && !Array.isArray(value)) {
return value; // do nothing if its already an object
export function parseNumeral(numOrString) {
const asNumber = Number(numOrString);
if (!isNaN(asNumber)) {
return asNumber;
}
return { value };
if (isNote(numOrString)) {
return toMidi(numOrString);
}
throw new Error(`cannot parse as numeral: "${numOrString}"`);
}
export function mapArgs(fn, mapFn) {
return (...args) => fn(...args.map(mapFn));
}
export function numeralArgs(fn) {
return mapArgs(fn, parseNumeral);
}
export function parseFractional(numOrString) {
const asNumber = Number(numOrString);
if (!isNaN(asNumber)) {
return asNumber;
}
const specialValue = {
pi: Math.PI,
w: 1,
h: 0.5,
q: 0.25,
e: 0.125,
s: 0.0625,
t: 1 / 3,
f: 0.2,
x: 1 / 6,
}[numOrString];
if (typeof specialValue !== 'undefined') {
return specialValue;
}
throw new Error(`cannot parse as fractional: "${numOrString}"`);
}
export const fractionalArgs = (fn) => mapArgs(fn, parseFractional);
+48
View File
@@ -0,0 +1,48 @@
// will move to https://github.com/felixroos/zyklus
// TODO: started flag
function createClock(
getTime,
callback, // called slightly before each cycle
duration = 0.05, // duration of each cycle
interval = 0.1, // interval between callbacks
overlap = 0.1, // overlap between callbacks
) {
let tick = 0; // counts callbacks
let phase = 0; // next callback time
let precision = 10 ** 4; // used to round phase
let minLatency = 0.01;
const setDuration = (setter) => (duration = setter(duration));
overlap = overlap || interval / 2;
const onTick = () => {
const t = getTime();
const lookahead = t + interval + overlap; // the time window for this tick
if (phase === 0) {
phase = t + minLatency;
}
// callback as long as we're inside the lookahead
while (phase < lookahead) {
phase = Math.round(phase * precision) / precision;
phase >= t && callback(phase, duration, tick);
phase < t && console.log('TOO LATE', phase); // what if latency is added from outside?
phase += duration; // increment phase by duration
tick++;
}
};
let intervalID;
const start = () => {
onTick();
intervalID = setInterval(onTick, interval * 1000);
};
const clear = () => clearInterval(intervalID);
const pause = () => clear();
const stop = () => {
tick = 0;
phase = 0;
clear();
};
const getPhase = () => phase;
// setCallback
return { setDuration, start, stop, pause, duration, getPhase };
}
export default createClock;
+16 -9
View File
@@ -10,15 +10,22 @@ Either install with `npm i @strudel.cycles/embed` or just use a cdn to import th
<script src="https://unpkg.com/@strudel.cycles/embed@latest"></script>
<strudel-repl>
<!--
"a4 [a3 c3] a3 c3".color('#F9D649')
.sub("<7 12 5 12>".slow(2))
.off(1/4,x=>x.add(7).color("#FFFFFF #0C3AA1 #C63928"))
.off(1/8,x=>x.add(12).color('#215CB6'))
.slow(2)
.legato(sine.range(0.3, 2).slow(28))
.wave("sawtooth square".fast(2))
.filter('lowpass', cosine.range(500,4000).slow(16))
.pianoroll({minMidi:20,maxMidi:120,background:'#202124'})
note(`[[e5 [b4 c5] d5 [c5 b4]]
[a4 [a4 c5] e5 [d5 c5]]
[b4 [~ c5] d5 e5]
[c5 a4 a4 ~]
[[~ d5] [~ f5] a5 [g5 f5]]
[e5 [~ c5] e5 [d5 c5]]
[b4 [b4 c5] d5 e5]
[c5 a4 a4 ~]],
[[e2 e3]*4]
[[a2 a3]*4]
[[g#2 g#3]*2 [e2 e3]*2]
[a2 a3 a2 a3 a2 a3 b1 c2]
[[d2 d3]*4]
[[c2 c3]*4]
[[b1 b2]*2 [e2 e3]*2]
[[a1 a2]*4]`).slow(16)
-->
</strudel-repl>
```
+16 -9
View File
@@ -2,14 +2,21 @@
<!-- <script src="./embed.js"></script> -->
<strudel-repl>
<!--
"a4 [a3 c3] a3 c3".color('#F9D649')
.sub("<7 12 5 12>".slow(2))
.off(1/4,x=>x.add(7).color("#FFFFFF #0C3AA1 #C63928"))
.off(1/8,x=>x.add(12).color('#215CB6'))
.slow(2)
.legato(sine.range(0.3, 2).slow(28))
.wave("sawtooth square".fast(2))
.filter('lowpass', cosine.range(500,4000).slow(16))
.pianoroll({minMidi:20,maxMidi:120,background:'#202124'})
note(`[[e5 [b4 c5] d5 [c5 b4]]
[a4 [a4 c5] e5 [d5 c5]]
[b4 [~ c5] d5 e5]
[c5 a4 a4 ~]
[[~ d5] [~ f5] a5 [g5 f5]]
[e5 [~ c5] e5 [d5 c5]]
[b4 [b4 c5] d5 e5]
[c5 a4 a4 ~]],
[[e2 e3]*4]
[[a2 a3]*4]
[[g#2 g#3]*2 [e2 e3]*2]
[a2 a3 a2 a3 a2 a3 b1 c2]
[[d2 d3]*4]
[[c2 c3]*4]
[[b1 b2]*2 [e2 e3]*2]
[[a1 a2]*4]`).slow(16)
-->
</strudel-repl>
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/embed",
"version": "0.1.1",
"version": "0.2.0",
"description": "Embeddable Web Component to load a Strudel REPL into an iframe",
"main": "embed.js",
"type": "module",
+2 -3
View File
@@ -11,10 +11,9 @@ npm i @strudel.cycles/eval --save
## Example
<!-- TODO: -extend +evalScope -->
```js
import { evaluate, extend } from '@strudel.cycles/eval';
import { evalScope } from '@strudel.cycles/core';
import { evaluate } from '@strudel.cycles/eval';
evalScope(
import('@strudel.cycles/core'),
+2 -39
View File
@@ -4,46 +4,9 @@ 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 as _evaluate } from '@strudel.cycles/core';
import shapeshifter from './shapeshifter.mjs';
import * as strudel from '@strudel.cycles/core';
const { isPattern, Pattern } = strudel;
export const extend = (...args) => {
console.warn('@strudel.cycles/eval extend is deprecated, please use evalScope instead');
Object.assign(globalThis, ...args);
};
let scoped = false;
export const evalScope = async (...args) => {
if (scoped) {
console.warn('@strudel.cycles/eval evalScope was called more than once.');
}
scoped = true;
const results = await Promise.allSettled(args);
const modules = results.filter((result) => result.status === 'fulfilled').map((r) => r.value);
results.forEach((result, i) => {
if (result.status === 'rejected') {
console.warn(`evalScope: module with index ${i} could not be loaded:`, result.reason);
}
});
Object.assign(globalThis, ...modules, Pattern.prototype.bootstrap());
};
function safeEval(str) {
return Function('"use strict";return (' + str + ')')();
}
export const evaluate = async (code) => {
if (!scoped) {
await evalScope(); // at least scope Pattern.prototype.boostrap
}
const shapeshifted = shapeshifter(code); // transform syntactically correct js code to semantically usable code
let evaluated = await safeEval(shapeshifted);
if (!isPattern(evaluated)) {
console.log('evaluated', evaluated);
const message = `got "${typeof evaluated}" instead of pattern`;
throw new Error(message + (typeof evaluated === 'function' ? ', did you forget to call a function?' : '.'));
}
return { mode: 'javascript', pattern: evaluated };
return _evaluate(code, shapeshifter);
};
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/eval",
"version": "0.2.0",
"version": "0.3.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
+6 -6
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/eval",
"version": "0.2.0",
"version": "0.3.2",
"description": "Code evaluator for strudel",
"main": "index.mjs",
"type": "module",
@@ -28,12 +28,12 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "^0.2.0",
"@strudel.cycles/core": "^0.3.2",
"estraverse": "^5.3.0",
"shift-ast": "^6.1.0",
"shift-codegen": "^7.0.3",
"shift-parser": "^7.0.3",
"shift-spec": "^2018.0.2",
"shift-ast": "^7.0.0",
"shift-codegen": "^8.1.0",
"shift-parser": "^8.0.0",
"shift-spec": "^2019.0.0",
"shift-traverser": "^1.0.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] })]);
+2 -2
View File
@@ -6,10 +6,10 @@ This program is free software: you can redistribute it and/or modify it under th
import { expect, describe, it } from 'vitest';
import { evaluate, evalScope } from '../evaluate.mjs';
import { evaluate } from '../evaluate.mjs';
import { mini } from '@strudel.cycles/mini';
import * as strudel from '@strudel.cycles/core';
const { fastcat } = strudel;
const { fastcat, evalScope } = strudel;
describe('evaluate', async () => {
await evalScope({ mini }, strudel);
+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])',
);
});
}
});
+43 -47
View File
@@ -4,11 +4,12 @@ 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 _WebMidi from 'webmidi';
import { Pattern, isPattern, isNote, getPlayableNoteValue, objectify } from '@strudel.cycles/core';
import { isNote } from 'tone';
import * as _WebMidi from 'webmidi';
import { Pattern, isPattern } from '@strudel.cycles/core';
import { Tone } from '@strudel.cycles/tone';
// if you use WebMidi from outside of this package, make sure to import that instance:
export const WebMidi = _WebMidi;
export const { WebMidi } = _WebMidi;
export function enableWebMidi() {
return new Promise((resolve, reject) => {
@@ -30,58 +31,53 @@ const outputByName = (name) => WebMidi.getOutputByName(name);
// Pattern.prototype.midi = function (output: string | number, channel = 1) {
Pattern.prototype.midi = function (output, channel = 1) {
if (output?.constructor?.name && isPattern(output?.constructor?.name)) {
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('${
WebMidi.outputs?.[0]?.name || 'IAC Driver Bus 1'
}')`,
);
}
return this.onTrigger((time, hap, currentTime) => {
let note;
try {
note = getPlayableNoteValue(hap);
} catch (err) {
// dont care if no note is found as we can still send cvs
}
const { velocity, nrpnn, nrpv, ccn, ccv, legato, duration: durationValue } = objectify(hap.value);
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 deadline = (time - currentTime) * 1000;
time = WebMidi.time + deadline;
/* const timingOffset = WebMidi.time - currentTime * 1000;
time = time * 1000 + timingOffset; */
const duration = (durationValue ?? hap.duration.valueOf()) * (legato ?? 1) * 1000 - 5;
if (note) {
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,
duration: hap.duration.valueOf() * 1000 - 5,
velocity,
});
}
if (ccn && ccv) {
device.sendControlChange(ccn, ccv, channel, { time });
}
};
return hap.setContext({ ...hap.context, onTrigger });
});
};
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/midi",
"version": "0.2.0",
"version": "0.3.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/midi",
"version": "0.2.0",
"version": "0.3.2",
"description": "Midi API for strudel",
"main": "index.mjs",
"repository": {
@@ -21,8 +21,8 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/tone": "^0.2.0",
"@strudel.cycles/tone": "^0.3.2",
"tone": "^14.7.77",
"webmidi": "^2.5.2"
"webmidi": "^3.0.21"
}
}
+9
View File
@@ -33,3 +33,12 @@ yields:
## Mini Notation API
See "Mini Notation" in the [Strudel Tutorial](https://strudel.tidalcycles.org/tutorial/)
## Building the Parser
The parser [krill-parser.js] is generated from [krill.pegjs](./krill.pegjs) using [peggy](https://peggyjs.org/).
To generate the parser, run
```js
npm run build:parser
```
+7 -4
View File
@@ -6,7 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th
import * as krill from './krill-parser.js';
import * as strudel from '@strudel.cycles/core';
import { addMiniLocations } from '@strudel.cycles/eval/shapeshifter.mjs';
// import { addMiniLocations } from '@strudel.cycles/eval/shapeshifter.mjs';
const { pure, Pattern, Fraction, stack, slowcat, sequence, timeCat, silence, reify } = strudel;
@@ -29,7 +29,10 @@ const applyOptions = (parent) => (pat, i) => {
case 'bjorklund':
return pat.euclid(operator.arguments_.pulse, operator.arguments_.step, operator.arguments_.rotation);
case 'degradeBy':
return reify(pat)._degradeByWith(strudel.rand.early(randOffset * _nextSeed()).segment(1), operator.arguments_.amount);
return reify(pat)._degradeByWith(
strudel.rand.early(randOffset * _nextSeed()).segment(1),
operator.arguments_.amount,
);
// TODO: case 'fixed-step': "%"
}
console.warn(`operator "${operator.type_}" not implemented`);
@@ -112,9 +115,9 @@ export function patternifyAST(ast) {
return silence;
}
if (typeof ast.source_ !== 'object') {
if (!addMiniLocations) {
/* if (!addMiniLocations) {
return ast.source_;
}
} */
if (!ast.location_) {
console.warn('no location for', ast);
return ast.source_;
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/mini",
"version": "0.2.0",
"version": "0.3.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/mini",
"version": "0.2.0",
"version": "0.3.2",
"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.2.0",
"@strudel.cycles/eval": "^0.2.0",
"@strudel.cycles/tone": "^0.2.0"
"@strudel.cycles/core": "^0.3.2",
"@strudel.cycles/eval": "^0.3.2",
"@strudel.cycles/tone": "^0.3.2"
},
"devDependencies": {
"peggy": "^2.0.1"
+2
View File
@@ -35,3 +35,5 @@ s("<bd sd> hh").osc()
```
or just [click here](http://localhost:3000/#cygiPGJkIHNkPiBoaCIpLm9zYygp)...
You can read more about [how to use Superdirt with Strudel the Tutorial](https://strudel.tidalcycles.org/tutorial/#superdirt-api)
+5 -1
View File
@@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import OSC from 'osc-js';
import { Pattern } from '@strudel.cycles/core';
import { parseNumeral, Pattern } from '@strudel.cycles/core';
const comm = new OSC();
comm.open();
@@ -31,6 +31,10 @@ Pattern.prototype.osc = function () {
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);
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/osc",
"version": "0.1.1",
"version": "0.2.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/osc",
"version": "0.1.1",
"version": "0.2.0",
"description": "OSC messaging for strudel",
"main": "osc.mjs",
"scripts": {
@@ -30,6 +30,6 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"osc-js": "^2.3.2"
"osc-js": "^2.4.0"
}
}
+1
View File
@@ -0,0 +1 @@
examples
+41 -2
View File
@@ -1,4 +1,43 @@
# @strudel.cycles/react
This package contains react hooks and components for strudel.
Example coming soon
This package contains react hooks and components for strudel. It is used internally by the Strudel REPL.
## Install
```js
npm i @strudel.cycles/react
```
## Usage
Here is a minimal example of how to set up a MiniRepl:
```jsx
import { evalScope, controls } from '@strudel.cycles/core';
import { MiniRepl } from '@strudel.cycles/react';
import { prebake } from '../repl/src/prebake.mjs';
evalScope(
controls,
import('@strudel.cycles/core'),
import('@strudel.cycles/tonal'),
import('@strudel.cycles/mini'),
import('@strudel.cycles/webaudio'),
/* probably import other strudel packages */
);
prebake();
export function Repl({ tune }) {
return <MiniRepl tune={tune} hideOutsideView={true} />;
}
```
## Development
If you change something in here and want to see the changes in the repl, make sure to run `npm run build` inside this folder!
```js
npm run dev # dev server
npm run build # build package
```
+2 -2
View File
File diff suppressed because one or more lines are too long
+387 -501
View File
@@ -1,582 +1,468 @@
import React, { useCallback, useState, useEffect, useMemo, useRef, useLayoutEffect } from 'react';
import _CodeMirror from '@uiw/react-codemirror';
import { Decoration, EditorView } from '@codemirror/view';
import { StateEffect, StateField } from '@codemirror/state';
import { javascript } from '@codemirror/lang-javascript';
import { tags } from '@lezer/highlight';
import { createTheme } from '@uiw/codemirror-themes';
import { useInView } from 'react-hook-inview';
import { evaluate } from '@strudel.cycles/eval';
import { Tone } from '@strudel.cycles/tone';
import { TimeSpan, State } from '@strudel.cycles/core';
import { webaudioOutputTrigger } from '@strudel.cycles/webaudio';
import { WebMidi, enableWebMidi } from '@strudel.cycles/midi';
var strudelTheme = createTheme({
theme: 'dark',
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({
theme: "dark",
settings: {
background: '#222',
foreground: '#75baff', // whats that?
caret: '#ffcc00',
selection: 'rgba(128, 203, 196, 0.5)',
selectionMatch: '#036dd626',
lineHighlight: '#8a91991a',
gutterBackground: 'transparent',
// gutterForeground: '#8a919966',
gutterForeground: '#676e95',
background: "#222",
foreground: "#75baff",
caret: "#ffcc00",
selection: "rgba(128, 203, 196, 0.5)",
selectionMatch: "#036dd626",
lineHighlight: "#8a91991a",
gutterBackground: "transparent",
gutterForeground: "#676e95"
},
styles: [
{ tag: tags.keyword, color: '#c792ea' },
{ tag: tags.operator, color: '#89ddff' },
{ tag: tags.special(tags.variableName), color: '#eeffff' },
{ tag: tags.typeName, color: '#f07178' },
{ tag: tags.atom, color: '#f78c6c' },
{ tag: tags.number, color: '#ff5370' },
{ tag: tags.definition(tags.variableName), color: '#82aaff' },
{ tag: tags.string, color: '#c3e88d' },
{ tag: tags.special(tags.string), color: '#f07178' },
{ tag: tags.comment, color: '#7d8799' },
{ tag: tags.variableName, color: '#f07178' },
{ tag: tags.tagName, color: '#ff5370' },
{ tag: tags.bracket, color: '#a2a1a4' },
{ tag: tags.meta, color: '#ffcb6b' },
{ tag: tags.attributeName, color: '#c792ea' },
{ tag: tags.propertyName, color: '#c792ea' },
{ tag: tags.className, color: '#decb6b' },
{ tag: tags.invalid, color: '#ffffff' },
],
{ 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" }
]
});
var style = '';
const setFlash = StateEffect.define();
const flashField = StateField.define({
const z = U.define(), ce = Q.define({
create() {
return Decoration.none;
return A.none;
},
update(flash2, tr) {
update(e, o) {
try {
for (let e of tr.effects) {
if (e.is(setFlash)) {
if (e.value) {
const mark = Decoration.mark({ attributes: { style: `background-color: #FFCA2880` } });
flash2 = Decoration.set([mark.range(0, tr.newDoc.length)]);
} else {
flash2 = Decoration.set([]);
}
}
}
return flash2;
} catch (err) {
console.warn("flash error", err);
return flash2;
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)]);
} else
e = A.set([]);
return e;
} catch (r) {
return console.warn("flash error", r), e;
}
},
provide: (f) => EditorView.decorations.from(f)
});
const flash = (view) => {
view.dispatch({ effects: setFlash.of(true) });
setTimeout(() => {
view.dispatch({ effects: setFlash.of(false) });
provide: (e) => K.decorations.from(e)
}), ie = (e) => {
e.dispatch({ effects: z.of(!0) }), setTimeout(() => {
e.dispatch({ effects: z.of(!1) });
}, 200);
};
const setHighlights = StateEffect.define();
const highlightField = StateField.define({
}, O = U.define(), le = Q.define({
create() {
return Decoration.none;
return A.none;
},
update(highlights, tr) {
update(e, o) {
try {
for (let e of tr.effects) {
if (e.is(setHighlights)) {
const marks = e.value.map(
(hap) => (hap.context.locations || []).map(({ start, end }) => {
const color = hap.context.color || "#FFCA28";
let from = tr.newDoc.line(start.line).from + start.column;
let to = tr.newDoc.line(end.line).from + end.column;
const l = tr.newDoc.length;
if (from > l || to > l) {
return;
}
const mark = Decoration.mark({ attributes: { style: `outline: 1.5px solid ${color};` } });
return mark.range(from, to);
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);
})
).flat().filter(Boolean) || [];
highlights = Decoration.set(marks, true);
e = A.set(n, !0);
}
}
return highlights;
} catch (err) {
return Decoration.set([]);
return e;
} catch {
return A.set([]);
}
},
provide: (f) => EditorView.decorations.from(f)
});
const extensions = [javascript(), strudelTheme, highlightField, flashField];
function CodeMirror({ value, onChange, onViewChanged, onSelectionChange, options, editorDidMount }) {
const handleOnChange = useCallback(
(value2) => {
onChange?.(value2);
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);
},
[onChange]
);
const handleOnCreateEditor = useCallback(
(view) => {
onViewChanged?.(view);
[o]
), t = T(
(c) => {
r?.(c);
},
[onViewChanged]
);
const handleOnUpdate = useCallback(
(viewUpdate) => {
if (viewUpdate.selectionSet && onSelectionChange) {
onSelectionChange?.(viewUpdate.state.selection);
}
[r]
), l = T(
(c) => {
c.selectionSet && n && n?.(c.state.selection);
},
[onSelectionChange]
[n]
);
return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(_CodeMirror, {
value,
onChange: handleOnChange,
onCreateEditor: handleOnCreateEditor,
onUpdate: handleOnUpdate,
extensions
return /* @__PURE__ */ y.createElement(y.Fragment, null, /* @__PURE__ */ y.createElement(Y, {
value: e,
onChange: a,
onCreateEditor: t,
onUpdate: l,
extensions: ue
}));
}
/*
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/>.
*/
/* 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());
});
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());
});
};
useEffect(() => {
ready && query();
}, [onEvent, onSchedule, onQuery, onDraw, ready]);
const start = async () => {
setStarted(true);
await Tone.start();
Tone.getTransport().start('+0.1');
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);
};
const stop = () => {
Tone.getTransport().pause();
setStarted(false);
};
const toggle = () => (started ? stop() : start());
return {
start,
stop,
onEvent,
started,
setStarted,
toggle,
query,
activeCycle,
start: p,
stop: d,
onEvent: o,
started: a,
setStarted: t,
toggle: () => a ? d() : p(),
query: f,
activeCycle: c
};
}
/*
usePostMessage.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/usePostMessage.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/>.
*/
function usePostMessage(listener) {
useEffect(() => {
window.addEventListener('message', listener);
return () => window.removeEventListener('message', listener);
}, [listener]);
return useCallback((data) => window.postMessage(data, '*'), []);
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 ? `
/*
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/>.
*/
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) => {
` : ""}${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 {
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
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);
}
},
[onEvent, pushLog],
[r, E]
),
onQuery: useCallback(
(state) => {
onQuery: T(
(b) => {
try {
return pattern?.query(state) || [];
} catch (err) {
console.warn(err);
err.message = 'query error: ' + err.message;
setError(err);
return [];
return h?.query(b) || [];
} catch (u) {
return console.warn(u), u.message = "query error: " + u.message, d(u), [];
}
},
[pattern],
[h]
),
onSchedule: useCallback((_events, cycle) => logCycle(_events), []),
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();
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 {
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);
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);
}
},
[activeCode, dirty, code, cycle, autolink, id, broadcast],
);
// logs events of cycle
const logCycle = (_events, cycle) => {
if (_events.length) ;
[t, _, i, P, o, s, B]
), J = (b, u) => {
b.length;
};
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,
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
};
}
/*
cx.js - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/cx.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/>.
*/
function cx(...classes) {
// : Array<string | undefined>
return classes.filter(Boolean).join(' ');
function $(...e) {
return e.filter(Boolean).join(" ");
}
let highlights = []; // actively highlighted events
let lastEnd;
function useHighlighting({ view, pattern, active }) {
useEffect(() => {
if (view) {
if (pattern && active) {
let frame = requestAnimationFrame(updateHighlights);
function updateHighlights() {
function ye({ view: e, pattern: o, active: r, getTime: n }) {
const s = I([]), i = I();
q(() => {
if (e)
if (o && r) {
let t = function() {
try {
const audioTime = Tone.getTransport().seconds;
// 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 || audioTime, audioTime - 1 / 10);
const span = [begin, audioTime + 1 / 60];
lastEnd = audioTime + 1 / 60;
highlights = highlights.filter((hap) => hap.whole.end > audioTime); // keep only highlights that are still active
const haps = pattern.queryArc(...span).filter((hap) => hap.hasOnset());
highlights = highlights.concat(haps); // add potential new onsets
view.dispatch({ effects: setHighlights.of(highlights) }); // highlight all still active + new active haps
} catch (err) {
// console.log('error in updateHighlights', err);
view.dispatch({ effects: setHighlights.of([]) });
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) });
} catch {
e.dispatch({ effects: O.of([]) });
}
frame = requestAnimationFrame(updateHighlights);
}
a = requestAnimationFrame(t);
}, a = requestAnimationFrame(t);
return () => {
cancelAnimationFrame(frame);
cancelAnimationFrame(a);
};
} else {
highlights = [];
view.dispatch({ effects: setHighlights.of([]) });
}
}
}, [pattern, active, view]);
} else
s.current = [], e.dispatch({ effects: O.of([]) });
}, [o, r, e]);
}
var tailwind = '';
const container = "_container_3i85k_1";
const header = "_header_3i85k_5";
const buttons = "_buttons_3i85k_9";
const button = "_button_3i85k_9";
const buttonDisabled = "_buttonDisabled_3i85k_17";
const error = "_error_3i85k_21";
const body = "_body_3i85k_25";
var styles = {
container: container,
header: header,
buttons: buttons,
button: button,
buttonDisabled: buttonDisabled,
error: error,
body: body
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
};
function Icon({ type }) {
return /* @__PURE__ */ React.createElement("svg", {
function W({ type: e }) {
return /* @__PURE__ */ y.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__ */ React.createElement("path", {
refresh: /* @__PURE__ */ y.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__ */ React.createElement("path", {
play: /* @__PURE__ */ y.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__ */ React.createElement("path", {
pause: /* @__PURE__ */ y.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"
})
}[type]);
}[e]);
}
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
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
});
useEffect(() => {
init && evaluateOnly();
}, [tune, init]);
const [view, setView] = useState();
const [ref, isVisible] = useInView({
q(() => {
r && f();
}, [e, r]);
const [v, h] = w(), [D, _] = te({
threshold: 0.01
});
const wasVisible = useRef();
const show = useMemo(() => {
if (isVisible || !hideOutsideView) {
wasVisible.current = true;
}
return isVisible || wasVisible.current;
}, [isVisible, hideOutsideView]);
useHighlighting({ view, pattern, active: cycle.started && !activeCode?.includes("strudel disable-highlighting") });
useLayoutEffect(() => {
if (enableKeyboard) {
const handleKeyPress = async (e) => {
if (e.ctrlKey || e.altKey) {
if (e.code === "Enter") {
e.preventDefault();
flash(view);
await activateCode();
} else if (e.code === "Period") {
cycle.stop();
e.preventDefault();
}
}
}), 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()));
};
window.addEventListener("keydown", handleKeyPress, true);
return () => window.removeEventListener("keydown", handleKeyPress, true);
return window.addEventListener("keydown", H, !0), () => window.removeEventListener("keydown", H, !0);
}
}, [enableKeyboard, pattern, code, activateCode, cycle, view]);
return /* @__PURE__ */ React.createElement("div", {
className: styles.container,
ref
}, /* @__PURE__ */ React.createElement("div", {
className: styles.header
}, /* @__PURE__ */ React.createElement("div", {
className: styles.buttons
}, /* @__PURE__ */ React.createElement("button", {
className: cx(styles.button, cycle.started ? "sc-animate-pulse" : ""),
onClick: () => togglePlay()
}, /* @__PURE__ */ React.createElement(Icon, {
type: cycle.started ? "pause" : "play"
})), /* @__PURE__ */ React.createElement("button", {
className: cx(dirty ? styles.button : styles.buttonDisabled),
onClick: () => activateCode()
}, /* @__PURE__ */ React.createElement(Icon, {
}, [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, {
type: "refresh"
}))), error && /* @__PURE__ */ React.createElement("div", {
className: styles.error
}, error.message)), /* @__PURE__ */ React.createElement("div", {
className: styles.body
}, show && /* @__PURE__ */ React.createElement(CodeMirror, {
value: code,
onChange: setCode,
onViewChanged: setView
}))), 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
})));
}
/*
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/>.
*/
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 };
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 };
}
export { CodeMirror, MiniRepl, cx, flash, useCycle, useHighlighting, usePostMessage, useRepl, useWebMidi };
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) };
}
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
};
+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-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: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}
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
!dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +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{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji"}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input:-ms-input-placeholder,textarea:-ms-input-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,:before,:after{--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: }.fixed{position:fixed}.absolute{position:absolute}.bottom-0{bottom:0px}.z-\[12\]{z-index:12}.flex{display:flex}.w-full{width:100%}.justify-center{justify-content:center}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(.5rem * var(--tw-space-x-reverse));margin-left:calc(.5rem * calc(1 - var(--tw-space-x-reverse)))}.rounded-t-md{border-top-left-radius:.375rem;border-top-right-radius:.375rem}.bg-slate-500{--tw-bg-opacity: 1;background-color:rgb(100 116 139 / var(--tw-bg-opacity))}.px-2{padding-left:.5rem;padding-right:.5rem}body{background:#123}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
import{f as s,i as t,h as o,j as d,q as f,l as p,k as u,p as i,o as l,n as r,w as g}from"./index.ec9f9930.js";export{s as getAudioContext,t as getCachedBuffer,o as getDestination,d as getLoadedBuffer,f as getLoadedSamples,p as loadBuffer,u as loadGithubSamples,i as panic,l as resetLoadedSamples,r as samples,g as webaudioOutput};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
import{P as w}from"./index.ec9f9930.js";var i,f=!1;async function b(a=38400){if(!f){if(f=!0,i)return i;if("serial"in navigator){const r=await navigator.serial.requestPort();await r.open({baudRate:a});const o=new TextEncoderStream;o.readable.pipeTo(r.writable);const s=o.writable.getWriter();i=function(e){s.write(e)}}else throw"Webserial is not available in this browser."}}const g=.1;w.prototype.serial=function(...a){return this._withHap(r=>{i||b(...a);const o=(s,e,u)=>{var t="";if(typeof e.value=="object")if("action"in e.value){t+=e.value.action+"(";var c=!0;for(const[n,l]of Object.entries(e.value))n!=="action"&&(c?c=!1:t+=",",t+=`${n}:${l}`);t+=")"}else for(const[n,l]of Object.entries(e.value))t+=`${n}:${l};`;else t=e.value;const v=(s-u+g)*1e3;window.setTimeout(i,v,t)};return r.setContext({...r.context,onTrigger:o})})};export{b as getWriter};
+14
View File
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Strudel Nano REPL</title>
<script type="module" crossorigin src="./assets/index.ec9f9930.js"></script>
<link rel="stylesheet" href="./assets/index.75f8960b.css">
</head>
<body>
<div id="root"></div>
</body>
</html>
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Strudel Nano REPL</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,24 @@
{
"name": "nano-repl",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "^18.0.17",
"@types/react-dom": "^18.0.6",
"@vitejs/plugin-react": "^2.0.1",
"autoprefixer": "^10.4.8",
"postcss": "^8.4.16",
"tailwindcss": "^3.1.8",
"vite": "^3.0.7"
}
}
@@ -0,0 +1,139 @@
import { evalScope, controls } from '@strudel.cycles/core';
import { getAudioContext, panic, webaudioOutput } from '@strudel.cycles/webaudio';
import { useCallback, useState } from 'react';
import CodeMirror, { flash } from '../../../src/components/CodeMirror6';
import useKeydown from '../../../src/hooks/useKeydown.mjs';
import useStrudel from '../../../src/hooks/useStrudel';
import useHighlighting from '../../../src/hooks/useHighlighting';
import './style.css';
// import { prebake } from '../../../../../repl/src/prebake.mjs';
// TODO: only import stuff when play is pressed?
evalScope(
controls,
import('@strudel.cycles/core'),
// import('@strudel.cycles/tone'),
// import('@strudel.cycles/midi'), // TODO: find out why midi loads tone.js
import('@strudel.cycles/tonal'),
import('@strudel.cycles/mini'),
import('@strudel.cycles/xen'),
import('@strudel.cycles/webaudio'),
import('@strudel.cycles/osc'),
import('@strudel.cycles/webdirt'),
import('@strudel.cycles/serial'),
import('@strudel.cycles/soundfonts'),
);
const defaultTune = `samples({
bd: ['bd/BT0AADA.wav','bd/BT0AAD0.wav','bd/BT0A0DA.wav','bd/BT0A0D3.wav','bd/BT0A0D0.wav','bd/BT0A0A7.wav'],
sd: ['sd/rytm-01-classic.wav','sd/rytm-00-hard.wav'],
hh: ['hh27/000_hh27closedhh.wav','hh/000_hh3closedhh.wav'],
}, 'github:tidalcycles/Dirt-Samples/master/');
stack(
s("bd,[~ <sd!3 sd(3,4,2)>],hh(3,4)") // drums
.speed(perlin.range(.7,.9)) // random sample speed variation
//.hush()
,"<a1 b1*2 a1(3,8) e2>" // bassline
.off(1/8,x=>x.add(12).degradeBy(.5)) // random octave jumps
.add(perlin.range(0,.5)) // random pitch variation
.superimpose(add(.05)) // add second, slightly detuned voice
.n() // wrap in "n"
.decay(.15).sustain(0) // make each note of equal length
.s('sawtooth') // waveform
.gain(.4) // turn down
.cutoff(sine.slow(7).range(300,5000)) // automate cutoff
//.hush()
,"<Am7!3 <Em7 E7b13 Em7 Ebm7b5>>".voicings() // chords
.superimpose(x=>x.add(.04)) // add second, slightly detuned voice
.add(perlin.range(0,.5)) // random pitch variation
.n() // wrap in "n"
.s('square') // waveform
.gain(.16) // turn down
.cutoff(500) // fixed cutoff
.attack(1) // slowly fade in
//.hush()
,"a4 c5 <e6 a6>".struct("x(5,8)")
.superimpose(x=>x.add(.04)) // add second, slightly detuned voice
.add(perlin.range(0,.5)) // random pitch variation
.n() // wrap in "n"
.decay(.1).sustain(0) // make notes short
.s('triangle') // waveform
.degradeBy(perlin.range(0,.5)) // randomly controlled random removal :)
.echoWith(4,.125,(x,n)=>x.gain(.15*1/(n+1))) // echo notes
//.hush()
)
.fast(2/3)`;
// await prebake();
const ctx = getAudioContext();
const getTime = () => ctx.currentTime;
function App() {
const [code, setCode] = useState(defaultTune);
const [view, setView] = useState();
// const [code, setCode] = useState(`"c3".note().slow(2)`);
const { scheduler, evaluate, schedulerError, evalError, isDirty, activeCode, pattern } = useStrudel({
code,
defaultOutput: webaudioOutput,
getTime,
});
useHighlighting({
view,
pattern,
active: !activeCode?.includes('strudel disable-highlighting'),
getTime: () => scheduler.phase,
});
const error = evalError || schedulerError;
useKeydown(
useCallback(
async (e) => {
if (e.ctrlKey || e.altKey) {
if (e.code === 'Enter') {
e.preventDefault();
flash(view);
await evaluate(code);
if (e.shiftKey) {
panic();
scheduler.stop();
scheduler.start();
}
if (!scheduler.started) {
scheduler.start();
}
} else if (e.code === 'Period') {
scheduler.pause();
panic();
e.preventDefault();
}
}
},
[scheduler, evaluate, view],
),
);
return (
<div>
{/* <textarea value={code} onChange={(e) => setCode(e.target.value)} cols="64" rows="30" /> */}
<nav className="z-[12] w-full flex justify-center absolute bottom-0">
<div className="bg-slate-500 space-x-2 px-2 rounded-t-md">
<button
onClick={async () => {
await evaluate(code);
await getAudioContext().resume();
scheduler.start();
}}
>
start
</button>
<button onClick={() => scheduler.stop()}>stop</button>
{isDirty && <button onClick={() => evaluate(code)}>eval</button>}
</div>
{error && <p>error {error.message}</p>}
</nav>
<CodeMirror value={code} onChange={setCode} onViewChanged={setView} />
</div>
);
}
export default App;
@@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
@@ -0,0 +1,7 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
background: #123;
}
@@ -0,0 +1,14 @@
/*
tailwind.config.js - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/tailwind.config.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/>.
*/
module.exports = {
// TODO: find out if leaving out tutorial path works now
content: ['./src/**/*.{js,jsx,ts,tsx}'],
theme: {
extend: {},
},
plugins: [],
};
@@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()]
})
+10 -10
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/react",
"version": "0.2.0",
"version": "0.3.2",
"description": "React components for strudel",
"main": "dist/index.cjs.js",
"module": "dist/index.es.js",
@@ -37,12 +37,12 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@codemirror/lang-javascript": "^6.0.2",
"@strudel.cycles/core": "^0.2.0",
"@strudel.cycles/eval": "^0.2.0",
"@strudel.cycles/tone": "^0.2.0",
"@uiw/codemirror-themes": "^4.11.4",
"@uiw/react-codemirror": "^4.11.4",
"@codemirror/lang-javascript": "^6.1.1",
"@strudel.cycles/core": "^0.3.2",
"@strudel.cycles/eval": "^0.3.2",
"@strudel.cycles/tone": "^0.3.2",
"@uiw/codemirror-themes": "^4.12.4",
"@uiw/react-codemirror": "^4.12.4",
"react-hook-inview": "^4.5.0"
},
"peerDependencies": {
@@ -52,12 +52,12 @@
"devDependencies": {
"@types/react": "^17.0.2",
"@types/react-dom": "^17.0.2",
"@vitejs/plugin-react": "^1.3.0",
"@vitejs/plugin-react": "^2.2.0",
"autoprefixer": "^10.4.7",
"postcss": "^8.4.13",
"postcss": "^8.4.18",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"tailwindcss": "^3.0.24",
"vite": "^2.9.9"
"vite": "^3.2.2"
}
}
+3 -2
View File
@@ -1,9 +1,10 @@
import React from 'react';
import { MiniRepl } from './components/MiniRepl';
import 'tailwindcss/tailwind.css';
import { evalScope } from '@strudel.cycles/eval';
import { controls, evalScope } from '@strudel.cycles/core';
evalScope(
controls,
import('@strudel.cycles/core'),
import('@strudel.cycles/tone'),
import('@strudel.cycles/tonal'),
@@ -16,7 +17,7 @@ evalScope(
function App() {
return (
<div>
<MiniRepl tune={`"c3"`} />
<MiniRepl tune={`note("c3")`} />
</div>
);
}
+7 -1
View File
@@ -8,6 +8,7 @@ import 'tailwindcss/tailwind.css';
import './style.css';
import styles from './MiniRepl.module.css';
import { Icon } from './Icon';
import { Tone } from '@strudel.cycles/tone';
export function MiniRepl({ tune, hideOutsideView = false, init, onEvent, enableKeyboard }) {
const { code, setCode, pattern, activeCode, activateCode, evaluateOnly, error, cycle, dirty, togglePlay, stop } =
@@ -30,7 +31,12 @@ export function MiniRepl({ tune, hideOutsideView = false, init, onEvent, enableK
}
return isVisible || wasVisible.current;
}, [isVisible, hideOutsideView]);
useHighlighting({ view, pattern, active: cycle.started && !activeCode?.includes('strudel disable-highlighting') });
useHighlighting({
view,
pattern,
active: cycle.started && !activeCode?.includes('strudel disable-highlighting'),
getTime: () => Tone.getTransport().seconds,
});
// set active pattern on ctrl+enter
useLayoutEffect(() => {
+11 -13
View File
@@ -1,11 +1,9 @@
import { useEffect } from 'react';
import { useEffect, useRef } from 'react';
import { setHighlights } from '../components/CodeMirror6';
import { Tone } from '@strudel.cycles/tone';
let highlights = []; // actively highlighted events
let lastEnd;
function useHighlighting({ view, pattern, active }) {
function useHighlighting({ view, pattern, active, getTime }) {
const highlights = useRef([]);
const lastEnd = useRef();
useEffect(() => {
if (view) {
if (pattern && active) {
@@ -13,16 +11,16 @@ function useHighlighting({ view, pattern, active }) {
function updateHighlights() {
try {
const audioTime = Tone.getTransport().seconds;
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 || audioTime, audioTime - 1 / 10);
const begin = Math.max(lastEnd.current || audioTime, audioTime - 1 / 10);
const span = [begin, audioTime + 1 / 60];
lastEnd = audioTime + 1 / 60;
highlights = highlights.filter((hap) => hap.whole.end > audioTime); // keep only highlights that are still active
lastEnd.current = audioTime + 1 / 60;
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 = highlights.concat(haps); // add potential new onsets
view.dispatch({ effects: setHighlights.of(highlights) }); // highlight all still active + new active haps
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([]) });
@@ -34,7 +32,7 @@ function useHighlighting({ view, pattern, active }) {
cancelAnimationFrame(frame);
};
} else {
highlights = [];
highlights.current = [];
view.dispatch({ effects: setHighlights.of([]) });
}
}
+10
View File
@@ -0,0 +1,10 @@
import { useLayoutEffect } from 'react';
// set active pattern on ctrl+enter
const useKeydown = (callback) =>
useLayoutEffect(() => {
window.addEventListener('keydown', callback, true);
return () => window.removeEventListener('keydown', callback, true);
}, [callback]);
export default useKeydown;
+43
View File
@@ -0,0 +1,43 @@
import { useRef, useCallback, useEffect, useMemo, useState } from 'react';
import { repl } from '@strudel.cycles/core/repl.mjs';
import { transpiler } from '@strudel.cycles/transpiler';
function useStrudel({ defaultOutput, interval, getTime, code, evalOnMount = false }) {
// scheduler
const [schedulerError, setSchedulerError] = useState();
const [evalError, setEvalError] = useState();
const [activeCode, setActiveCode] = useState(code);
const [pattern, setPattern] = useState();
const isDirty = code !== activeCode;
const { scheduler, evaluate: _evaluate } = useMemo(
() =>
repl({
interval,
defaultOutput,
onSchedulerError: setSchedulerError,
onEvalError: setEvalError,
getTime,
transpiler,
onEval: ({ pattern: _pattern, code }) => {
setActiveCode(code);
setPattern(_pattern);
setEvalError();
},
onEvalError: setEvalError,
}),
[defaultOutput, interval, getTime],
);
const evaluate = useCallback(() => _evaluate(code), [_evaluate, code]);
const inited = useRef();
useEffect(() => {
if (!inited.current && evalOnMount && code) {
inited.current = true;
evaluate();
}
}, [evaluate, evalOnMount, code]);
return { schedulerError, scheduler, evalError, evaluate, activeCode, isDirty, pattern };
}
export default useStrudel;
+2
View File
@@ -6,5 +6,7 @@ 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';
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/soundfonts",
"version": "0.2.0",
"version": "0.3.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/soundfonts",
"version": "0.2.1",
"version": "0.3.2",
"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.2.0",
"@strudel.cycles/webaudio": "^0.2.0",
"@strudel.cycles/core": "^0.3.2",
"@strudel.cycles/webaudio": "^0.3.2",
"sfumato": "^0.1.2",
"soundfont2": "^0.4.0"
},
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/tonal",
"version": "0.2.0",
"version": "0.3.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/tonal",
"version": "0.2.0",
"version": "0.3.2",
"description": "Tonal functions for strudel",
"main": "index.mjs",
"type": "module",
@@ -25,8 +25,8 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "^0.2.0",
"@strudel.cycles/core": "^0.3.2",
"@tonaljs/tonal": "^4.6.5",
"webmidi": "^3.0.15"
"webmidi": "^3.0.21"
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/tone",
"version": "0.2.0",
"version": "0.3.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
+2 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/tone",
"version": "0.2.0",
"version": "0.3.2",
"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.2.0",
"@tonejs/piano": "^0.2.1",
"@strudel.cycles/core": "^0.3.2",
"chord-voicings": "^0.0.1",
"tone": "^14.7.77"
}
-11
View File
@@ -30,8 +30,6 @@ const {
getDestination,
Players,
} = Tone;
import * as tonePiano from '@tonejs/piano';
const { Piano } = tonePiano;
import { getPlayableNoteValue } from '@strudel.cycles/core/util.mjs';
// "balanced" | "interactive" | "playback";
@@ -61,10 +59,6 @@ Pattern.prototype.tone = function (instrument) {
instrument.triggerAttack(note, time);
} else if (instrument instanceof NoiseSynth) {
instrument.triggerAttackRelease(hap.duration.valueOf(), time); // noise has no value
} else if (instrument instanceof Piano) {
note = getPlayableNoteValue(hap);
instrument.keyDown({ note, time, velocity });
instrument.keyUp({ note, time: time + hap.duration.valueOf(), velocity });
} else if (instrument instanceof Sampler) {
note = getPlayableNoteValue(hap);
instrument.triggerAttackRelease(note, hap.duration.valueOf(), time, velocity);
@@ -110,11 +104,6 @@ export const players = (options, baseUrl = '') => {
});
};
export const synth = (options) => new Synth(options);
export const piano = async (options = { velocities: 1 }) => {
const p = new Piano(options);
await p.load();
return p;
};
// effect helpers
export const vol = (v) => new Gain(v);
+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.3.2",
"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.3.2",
"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,
};
}
-71
View File
@@ -1,71 +0,0 @@
/*
clockworker.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/webaudio/clockworker.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/>.
*/
// helpers to create a worker dynamically without needing a server / extra file
const stringifyFunction = (func) => '(' + func + ')();';
const urlifyFunction = (func) => URL.createObjectURL(new Blob([stringifyFunction(func)], { type: 'text/javascript' }));
const createWorker = (func) => new Worker(urlifyFunction(func));
// this is just a setInterval with a counter, running in a worker
export class ClockWorker {
worker;
interval = 0.1; // query span
tick = 0;
constructor(callback, interval = this.interval) {
this.interval = interval;
this.worker = createWorker(() => {
// we cannot use closures here!
let interval;
let timerID = null; // this is clock #1 (the sloppy js clock)
const clear = () => {
if (timerID) {
clearInterval(timerID);
timerID = null;
}
};
const start = () => {
clear();
if (!interval) {
throw new Error('no interval set! call worker.postMessage({interval}) before starting.');
}
postMessage('tick');
timerID = setInterval(() => postMessage('tick'), interval * 1000);
};
self.onmessage = function (e) {
if (e.data == 'start') {
start();
} else if (e.data.interval) {
interval = e.data.interval;
if (timerID) {
start();
}
} else if (e.data == 'stop') {
clear();
}
};
});
this.worker.postMessage({ interval });
// const round = (n, d) => Math.round(n * d) / d;
this.worker.onmessage = (e) => {
if (e.data === 'tick') {
// callback with query span, using clock #2 (the audio clock)
callback(this.tick++, this.interval);
}
};
}
start() {
// console.log('start...');
this.worker.postMessage('start');
}
stop() {
// console.log('stop...');
this.worker.postMessage('stop');
this.tick = 0;
}
setInterval(interval) {
this.worker.postMessage({ interval });
}
}
-67
View File
@@ -1,67 +0,0 @@
<div style="position: absolute; top: 0; right: 0; padding: 4px">
<button id="start" style="margin-bottom: 4px; font-size: 2em">start</button><br />
<button id="stop" style="margin-bottom: 4px; font-size: 2em">stop</button><br />
<button id="slower" style="font-size: 2em">-</button>
<button id="faster" style="font-size: 2em">+</button>
</div>
<textarea
style="font-size: 2em; background: #052b49; color: #fff; height: 100%; width: 100%; outline: none; border: 0"
id="text"
spellcheck="false"
>
Loading...</textarea
>
<script type="module">
document.body.style = 'margin: 0';
import * as strudel from '@strudel.cycles/core';
import * as util from '@strudel.cycles/core/util.mjs';
import '@strudel.cycles/core/euclid.mjs';
import { Scheduler, getAudioContext } from '../index.mjs';
const { cat, State, TimeSpan } = strudel;
Object.assign(window, strudel); // add strudel to eval scope
const scheduler = new Scheduler({
audioContext: getAudioContext(),
interval: 0.1,
onEvent: (e) => {
e.context?.createAudioNode?.(e);
},
});
let initialCode = `sequence(1,2).mul(55/2) // frequencies
.mul(slowcat(1,2))
.mul(slowcat(1,3/2,4/3,5/3).slow(8))
.fast(3)
.freq()
.velocity(.5)
.s('sawtooth')
.cutoff(800)
.out()
`;
try {
const base64 = decodeURIComponent(window.location.href.split('#')[1]);
initialCode = atob(base64);
} catch (err) {
console.warn('failed to decode', err);
}
const input = document.getElementById('text');
input.value = initialCode;
const evaluate = () => {
try {
const pattern = eval(input.value);
scheduler.setPattern(pattern);
window.location.hash = '#' + encodeURIComponent(btoa(input.value)); // update url hash
} catch (err) {
console.warn(err);
}
};
evaluate();
input.addEventListener('input', () => evaluate());
document.getElementById('start').addEventListener('click', () => scheduler.start());
document.getElementById('stop').addEventListener('click', () => scheduler.stop());
document.getElementById('slower').addEventListener('click', () => scheduler.setCps(scheduler.cps - 0.1));
document.getElementById('faster').addEventListener('click', () => scheduler.setCps(scheduler.cps + 0.1));
</script>

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