Merge commit '47717e872b87f497990d7c1d6c3d54bfb96f2003' into paper

This commit is contained in:
Felix Roos 2022-04-24 21:00:54 +02:00
commit a32d77b24d
58 changed files with 5006 additions and 2438 deletions

3
.gitignore vendored
View File

@ -26,4 +26,5 @@ node_modules/
.DS_Store
repl-parcel
mytunes.ts
doc
doc
.parcel-cache

View File

@ -1,16 +1,16 @@
{
"files": {
"main.css": "/static/css/main.0d689283.css",
"main.js": "/static/js/main.194ee673.js",
"main.js": "/static/js/main.7e790d7f.js",
"static/js/787.1c52cb78.chunk.js": "/static/js/787.1c52cb78.chunk.js",
"static/media/logo.svg": "/static/media/logo.ac95051720b3dccfe511e0e02d8e1029.svg",
"index.html": "/index.html",
"main.0d689283.css.map": "/static/css/main.0d689283.css.map",
"main.194ee673.js.map": "/static/js/main.194ee673.js.map",
"main.7e790d7f.js.map": "/static/js/main.7e790d7f.js.map",
"787.1c52cb78.chunk.js.map": "/static/js/787.1c52cb78.chunk.js.map"
},
"entrypoints": [
"static/css/main.0d689283.css",
"static/js/main.194ee673.js"
"static/js/main.7e790d7f.js"
]
}

View File

@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Strudel REPL"/><title>Strudel REPL</title><script defer="defer" src="/static/js/main.194ee673.js"></script><link href="/static/css/main.0d689283.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Strudel REPL"/><title>Strudel REPL</title><script defer="defer" src="/static/js/main.7e790d7f.js"></script><link href="/static/css/main.0d689283.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>

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

3
docs/static/js/main.7e790d7f.js vendored Normal file

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><link rel="icon" href="/tutorial/favicon.e3ab9dd9.ico"><link rel="stylesheet" type="text/css" href="/tutorial/index.757e9f9d.css"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="description" content="Strudel REPL"><title>Strudel Tutorial</title></head><body> <div id="root"></div> <noscript>You need to enable JavaScript to run this app.</noscript> <script src="/tutorial/index.3b5f65fb.js" defer></script> </body></html>
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><link rel="icon" href="/tutorial/favicon.e3ab9dd9.ico"><link rel="stylesheet" type="text/css" href="/tutorial/index.757e9f9d.css"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="description" content="Strudel REPL"><title>Strudel Tutorial</title></head><body> <div id="root"></div> <noscript>You need to enable JavaScript to run this app.</noscript> <script src="/tutorial/index.a0cd6c24.js" defer></script> </body></html>

View File

@ -1,14 +0,0 @@
<!DOCTYPE html>
<script>import("./strudel.js").then(m => strudel = m)
</script>
<html>
<head>
<title>Bingo</title>
</head>
<body>
<button onclick="foo()">hello</button>
<button onclick="bar()">hello</button>
<script type="module" src="./strudel.js"></script>
</body>
</html>

331
package-lock.json generated
View File

@ -1889,6 +1889,10 @@
"resolved": "packages/osc",
"link": true
},
"node_modules/@strudel.cycles/serial": {
"resolved": "packages/serial",
"link": true
},
"node_modules/@strudel.cycles/tonal": {
"resolved": "packages/tonal",
"link": true
@ -1897,6 +1901,10 @@
"resolved": "packages/tone",
"link": true
},
"node_modules/@strudel.cycles/webaudio": {
"resolved": "packages/webaudio",
"link": true
},
"node_modules/@strudel.cycles/xen": {
"resolved": "packages/xen",
"link": true
@ -2276,17 +2284,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ansi-gray": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/ansi-gray/-/ansi-gray-0.1.1.tgz",
"integrity": "sha1-KWLPVOyXksSFEKPetSRDaGHvclE=",
"dependencies": {
"ansi-wrap": "0.1.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
@ -2311,14 +2308,6 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/ansi-wrap": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz",
"integrity": "sha1-qCJQ3bABXponyoLoLqYDu/pF768=",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/anymatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
@ -2941,14 +2930,6 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"node_modules/color-support": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
"integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==",
"bin": {
"color-support": "bin.js"
}
},
"node_modules/columnify": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/columnify/-/columnify-1.6.0.tgz",
@ -3787,35 +3768,6 @@
"node": ">=0.10.0"
}
},
"node_modules/extsprintf": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
"integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=",
"dev": true,
"engines": [
"node >=0.6.0"
]
},
"node_modules/fancy-log": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-1.3.3.tgz",
"integrity": "sha512-k9oEhlyc0FrVh25qYuSELjr8oxsCoc4/LEZfg2iJJrfEk/tZL9bCoJE47gqAvI2m/AUjluCS4+3I0eTx8n3AEw==",
"dependencies": {
"ansi-gray": "^0.1.1",
"color-support": "^1.1.3",
"parse-node-version": "^1.0.0",
"time-stamp": "^1.0.0"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true
},
"node_modules/fast-glob": {
"version": "3.2.11",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz",
@ -5135,14 +5087,6 @@
"node": ">=0.10.0"
}
},
"node_modules/isomorphic-ws": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz",
"integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==",
"peerDependencies": {
"ws": "*"
}
},
"node_modules/isstream": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
@ -5237,48 +5181,6 @@
"node": ">=4"
}
},
"node_modules/json-parse-better-errors": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
"integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==",
"dev": true
},
"node_modules/json-parse-even-better-errors": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
"dev": true
},
"node_modules/json-schema": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
"integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==",
"dev": true
},
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true
},
"node_modules/json-stringify-safe": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
"integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=",
"dev": true
},
"node_modules/json5": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
"peer": true,
"bin": {
"json5": "lib/cli.js"
},
"engines": {
"node": ">=6"
}
},
"node_modules/jsonfile": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
@ -6716,12 +6618,11 @@
}
},
"node_modules/osc-js": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/osc-js/-/osc-js-2.3.1.tgz",
"integrity": "sha512-DjpfUcyTsMmD7uLdyjqsT9zwuNkUOG8yJMc56H9spTCRqTls5vLt5QnlVploVqSRwQ2stvcc+CsY18ouxab9mg==",
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/osc-js/-/osc-js-2.3.2.tgz",
"integrity": "sha512-9i7J4u1hH+glooGMh+ki1ni0JGqKmylT8r0nXKugHbRK63rR+kl4O+5tGW6+/EszjbCju3KV+eXQQzFDdGrmhg==",
"dependencies": {
"isomorphic-ws": "4.0.1",
"ws": "8.5.0"
"ws": "^8.5.0"
}
},
"node_modules/osenv": {
@ -6933,14 +6834,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/parse-node-version": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz",
"integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/parse-path": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/parse-path/-/parse-path-4.0.3.tgz",
@ -7498,15 +7391,6 @@
"node": ">=4"
}
},
"node_modules/read-pkg/node_modules/pify": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
"integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
"dev": true,
"engines": {
"node": ">=4"
}
},
"node_modules/read-pkg/node_modules/semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
@ -8487,14 +8371,6 @@
"node": ">= 6"
}
},
"node_modules/time-stamp": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/time-stamp/-/time-stamp-1.1.0.tgz",
"integrity": "sha1-dkpaEa9QVhkhsTPztE5hhofg9cM=",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/tmp": {
"version": "0.0.33",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
@ -9251,7 +9127,7 @@
},
"packages/core": {
"name": "@strudel.cycles/core",
"version": "0.0.3",
"version": "0.0.5",
"license": "GPL-3.0-or-later",
"dependencies": {
"bjork": "^0.0.1",
@ -9263,10 +9139,10 @@
},
"packages/eval": {
"name": "@strudel.cycles/eval",
"version": "0.0.3",
"version": "0.0.5",
"license": "GPL-3.0-or-later",
"dependencies": {
"@strudel.cycles/core": "^0.0.3",
"@strudel.cycles/core": "^0.0.5",
"estraverse": "^5.3.0",
"shift-ast": "^6.1.0",
"shift-codegen": "^7.0.3",
@ -9290,37 +9166,42 @@
},
"packages/midi": {
"name": "@strudel.cycles/midi",
"version": "0.0.4",
"version": "0.0.6",
"license": "GPL-3.0-or-later",
"dependencies": {
"@strudel.cycles/tone": "^0.0.4",
"@strudel.cycles/tone": "^0.0.6",
"tone": "^14.7.77",
"webmidi": "^2.5.2"
}
},
"packages/mini": {
"name": "@strudel.cycles/mini",
"version": "0.0.4",
"version": "0.0.7",
"license": "GPL-3.0-or-later",
"dependencies": {
"@strudel.cycles/eval": "^0.0.3",
"@strudel.cycles/tone": "^0.0.4"
"@strudel.cycles/core": "^0.0.5",
"@strudel.cycles/eval": "^0.0.5",
"@strudel.cycles/tone": "^0.0.6"
}
},
"packages/osc": {
"name": "@strudel.cycles/osc",
"version": "0.0.1",
"version": "0.0.2",
"license": "GPL-3.0-or-later",
"dependencies": {
"osc-js": "^2.3.0"
"osc-js": "^2.3.2"
}
},
"packages/serial": {
"version": "0.0.6",
"license": "GPL-3.0-or-later"
},
"packages/tonal": {
"name": "@strudel.cycles/tonal",
"version": "0.0.3",
"version": "0.0.5",
"license": "GPL-3.0-or-later",
"dependencies": {
"@strudel.cycles/core": "^0.0.3",
"@strudel.cycles/core": "^0.0.5",
"@tonaljs/tonal": "^4.6.5",
"webmidi": "^3.0.15"
}
@ -9342,21 +9223,29 @@
},
"packages/tone": {
"name": "@strudel.cycles/tone",
"version": "0.0.4",
"version": "0.0.6",
"license": "GPL-3.0-or-later",
"dependencies": {
"@strudel.cycles/core": "^0.0.3",
"@strudel.cycles/core": "^0.0.5",
"@tonejs/piano": "^0.2.1",
"chord-voicings": "^0.0.1",
"tone": "^14.7.77"
}
},
"packages/xen": {
"name": "@strudel.cycles/xen",
"version": "0.0.3",
"packages/webaudio": {
"name": "@strudel.cycles/webaudio",
"version": "0.0.6",
"license": "GPL-3.0-or-later",
"dependencies": {
"@strudel.cycles/core": "^0.0.3"
"@strudel.cycles/core": "^0.0.5"
}
},
"packages/xen": {
"name": "@strudel.cycles/xen",
"version": "0.0.5",
"license": "GPL-3.0-or-later",
"dependencies": {
"@strudel.cycles/core": "^0.0.5"
}
}
},
@ -10872,7 +10761,7 @@
"@strudel.cycles/eval": {
"version": "file:packages/eval",
"requires": {
"@strudel.cycles/core": "^0.0.3",
"@strudel.cycles/core": "^0.0.5",
"estraverse": "^5.3.0",
"shift-ast": "^6.1.0",
"shift-codegen": "^7.0.3",
@ -10884,7 +10773,7 @@
"@strudel.cycles/midi": {
"version": "file:packages/midi",
"requires": {
"@strudel.cycles/tone": "^0.0.4",
"@strudel.cycles/tone": "^0.0.6",
"tone": "^14.7.77",
"webmidi": "^2.5.2"
}
@ -10892,20 +10781,24 @@
"@strudel.cycles/mini": {
"version": "file:packages/mini",
"requires": {
"@strudel.cycles/eval": "^0.0.3",
"@strudel.cycles/tone": "^0.0.4"
"@strudel.cycles/core": "^0.0.5",
"@strudel.cycles/eval": "^0.0.5",
"@strudel.cycles/tone": "^0.0.6"
}
},
"@strudel.cycles/osc": {
"version": "file:packages/osc",
"requires": {
"osc-js": "^2.3.0"
"osc-js": "^2.3.2"
}
},
"@strudel.cycles/serial": {
"version": "file:packages/serial"
},
"@strudel.cycles/tonal": {
"version": "file:packages/tonal",
"requires": {
"@strudel.cycles/core": "^0.0.3",
"@strudel.cycles/core": "^0.0.5",
"@tonaljs/tonal": "^4.6.5",
"webmidi": "^3.0.15"
},
@ -10925,16 +10818,22 @@
"@strudel.cycles/tone": {
"version": "file:packages/tone",
"requires": {
"@strudel.cycles/core": "^0.0.3",
"@strudel.cycles/core": "^0.0.5",
"@tonejs/piano": "^0.2.1",
"chord-voicings": "^0.0.1",
"tone": "^14.7.77"
}
},
"@strudel.cycles/webaudio": {
"version": "file:packages/webaudio",
"requires": {
"@strudel.cycles/core": "^0.0.5"
}
},
"@strudel.cycles/xen": {
"version": "file:packages/xen",
"requires": {
"@strudel.cycles/core": "^0.0.3"
"@strudel.cycles/core": "^0.0.5"
}
},
"@tonaljs/abc-notation": {
@ -11285,14 +11184,6 @@
"type-fest": "^0.21.3"
}
},
"ansi-gray": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/ansi-gray/-/ansi-gray-0.1.1.tgz",
"integrity": "sha1-KWLPVOyXksSFEKPetSRDaGHvclE=",
"requires": {
"ansi-wrap": "0.1.0"
}
},
"ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
@ -11308,11 +11199,6 @@
"color-convert": "^2.0.1"
}
},
"ansi-wrap": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz",
"integrity": "sha1-qCJQ3bABXponyoLoLqYDu/pF768="
},
"anymatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
@ -11792,11 +11678,6 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"color-support": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
"integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg=="
},
"columnify": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/columnify/-/columnify-1.6.0.tgz",
@ -12453,29 +12334,6 @@
}
}
},
"extsprintf": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
"integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=",
"dev": true
},
"fancy-log": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-1.3.3.tgz",
"integrity": "sha512-k9oEhlyc0FrVh25qYuSELjr8oxsCoc4/LEZfg2iJJrfEk/tZL9bCoJE47gqAvI2m/AUjluCS4+3I0eTx8n3AEw==",
"requires": {
"ansi-gray": "^0.1.1",
"color-support": "^1.1.3",
"parse-node-version": "^1.0.0",
"time-stamp": "^1.0.0"
}
},
"fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true
},
"fast-glob": {
"version": "3.2.11",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz",
@ -13462,12 +13320,6 @@
"integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
"dev": true
},
"isomorphic-ws": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz",
"integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==",
"requires": {}
},
"isstream": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
@ -13543,42 +13395,6 @@
"integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
"peer": true
},
"json-parse-better-errors": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
"integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==",
"dev": true
},
"json-parse-even-better-errors": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
"dev": true
},
"json-schema": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
"integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==",
"dev": true
},
"json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true
},
"json-stringify-safe": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
"integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=",
"dev": true
},
"json5": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
"peer": true
},
"jsonfile": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
@ -14694,12 +14510,11 @@
"dev": true
},
"osc-js": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/osc-js/-/osc-js-2.3.1.tgz",
"integrity": "sha512-DjpfUcyTsMmD7uLdyjqsT9zwuNkUOG8yJMc56H9spTCRqTls5vLt5QnlVploVqSRwQ2stvcc+CsY18ouxab9mg==",
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/osc-js/-/osc-js-2.3.2.tgz",
"integrity": "sha512-9i7J4u1hH+glooGMh+ki1ni0JGqKmylT8r0nXKugHbRK63rR+kl4O+5tGW6+/EszjbCju3KV+eXQQzFDdGrmhg==",
"requires": {
"isomorphic-ws": "4.0.1",
"ws": "8.5.0"
"ws": "^8.5.0"
}
},
"osenv": {
@ -14845,11 +14660,6 @@
"lines-and-columns": "^1.1.6"
}
},
"parse-node-version": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz",
"integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA=="
},
"parse-path": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/parse-path/-/parse-path-4.0.3.tgz",
@ -15230,12 +15040,6 @@
"pify": "^3.0.0"
}
},
"pify": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
"integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
"dev": true
},
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
@ -16066,11 +15870,6 @@
}
}
},
"time-stamp": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/time-stamp/-/time-stamp-1.1.0.tgz",
"integrity": "sha1-dkpaEa9QVhkhsTPztE5hhofg9cM="
},
"tmp": {
"version": "0.0.33",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",

View File

@ -9,7 +9,8 @@
"bootstrap": "lerna bootstrap",
"setup": "npm i && npm run bootstrap && cd repl && npm i",
"repl": "cd repl && npm run start",
"osc": "cd packages/osc && npm run server"
"osc": "cd packages/osc && npm run server",
"build": "cd repl && npm run build"
},
"workspaces": [
"packages/*"

View File

@ -11,7 +11,7 @@ npm i @strudel.cycles/core --save
## Example
```js
import { sequence, State, TimeSpan } from '@strudel.cycles/core';
import { sequence } from '@strudel.cycles/core';
const pattern = sequence('a', ['b', 'c']);

View File

@ -276,14 +276,14 @@ const generic_params = [
const _name = (name, ...pats) => sequence(...pats).withValue((x) => ({ [name]: x }));
const _unionise = (func) =>
const _setter = (func) =>
function (...pats) {
return this.union(func(...pats));
return this.set(func(...pats));
};
generic_params.forEach(([type, name, description]) => {
controls[name] = (...pats) => _name(name, ...pats);
Pattern.prototype[name] = _unionise(controls[name]);
Pattern.prototype[name] = _setter(controls[name]);
});
export default controls;

View File

@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/core",
"version": "0.0.3",
"version": "0.0.5",
"lockfileVersion": 2,
"requires": true,
"packages": {

View File

@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/core",
"version": "0.0.3",
"version": "0.0.5",
"description": "Port of Tidal Cycles to JavaScript",
"main": "index.mjs",
"type": "module",
@ -30,5 +30,6 @@
},
"devDependencies": {
"mocha": "^9.2.2"
}
},
"gitHead": "0e26d4e741500f5bae35b023608f062a794905c2"
}

View File

@ -3,7 +3,7 @@ import Fraction from './fraction.mjs';
import Hap from './hap.mjs';
import State from './state.mjs';
import { isNote, toMidi, compose, removeUndefineds, flatten, id, listRange, curry } from './util.mjs';
import { isNote, toMidi, compose, removeUndefineds, flatten, id, listRange, curry, mod } from './util.mjs';
export class Pattern {
// the following functions will get patternFactories as nested functions:
@ -179,14 +179,16 @@ export class Pattern {
const query = function (state) {
const haps = [];
for (const hap_func of pat_func.query(state)) {
const event_vals = pat_val.query(state.setSpan(hap_func.part));
const event_vals = pat_val.query(state.setSpan(hap_func.wholeOrPart()));
for (const hap_val of event_vals) {
const new_whole = hap_func.whole;
const new_part = hap_func.part.intersection_e(hap_val.part);
const new_value = hap_func.value(hap_val.value);
const new_context = hap_val.combineContext(hap_func);
const hap = new Hap(new_whole, new_part, new_value, new_context);
haps.push(hap);
const new_part = hap_func.part.intersection(hap_val.part);
if (new_part) {
const new_value = hap_func.value(hap_val.value);
const new_context = hap_val.combineContext(hap_func);
const hap = new Hap(new_whole, new_part, new_value, new_context);
haps.push(hap);
}
}
}
return haps;
@ -200,14 +202,16 @@ export class Pattern {
const query = function (state) {
const haps = [];
for (const hap_val of pat_val.query(state)) {
const hap_funcs = pat_func.query(state.setSpan(hap_val.part));
const hap_funcs = pat_func.query(state.setSpan(hap_val.wholeOrPart()));
for (const hap_func of hap_funcs) {
const new_whole = hap_val.whole;
const new_part = hap_func.part.intersection_e(hap_val.part);
const new_value = hap_func.value(hap_val.value);
const new_context = hap_val.combineContext(hap_func);
const hap = new Hap(new_whole, new_part, new_value, new_context);
haps.push(hap);
const new_part = hap_func.part.intersection(hap_val.part);
if (new_part) {
const new_value = hap_func.value(hap_val.value);
const new_context = hap_val.combineContext(hap_func);
const hap = new Hap(new_whole, new_part, new_value, new_context);
haps.push(hap);
}
}
}
return haps;
@ -243,9 +247,24 @@ export class Pattern {
);
}
_opleft(other, func) {
_opLeft(other, func) {
return this.fmap(func).appLeft(reify(other));
}
_opRight(other, func) {
return this.fmap(func).appRight(reify(other));
}
_opBoth(other, func) {
return this.fmap(func).appBoth(reify(other));
}
_opSqueeze(other, func) {
const otherPat = reify(other);
return this.fmap((a) => otherPat.fmap((b) => func(a)(b)))._squeezeJoin();
}
_opSqueezeFlip(other, func) {
const thisPat = this;
const otherPat = reify(other);
return otherPat.fmap((a) => thisPat.fmap((b) => func(b)(a)))._squeezeJoin();
}
_asNumber(silent = false) {
return this._withEvent((event) => {
@ -271,22 +290,6 @@ export class Pattern {
})._removeUndefineds();
}
add(other) {
return this._asNumber()._opleft(other, (a) => (b) => a + b);
}
sub(other) {
return this._asNumber()._opleft(other, (a) => (b) => a - b);
}
mul(other) {
return this._asNumber()._opleft(other, (a) => (b) => a * b);
}
div(other) {
return this._asNumber()._opleft(other, (a) => (b) => a / b);
}
round() {
return this._asNumber().fmap((v) => Math.round(v));
}
@ -311,15 +314,15 @@ export class Pattern {
return this.mul(max - min).add(min);
}
rangex(min, max) {
return this.range(Math.log(min), Math.log(max)).fmap(Math.exp);
}
// Assumes source pattern of numbers in range -1..1
range2(min, max) {
return this._fromBipolar().range(min, max);
}
union(other) {
return this._opleft(other, (a) => (b) => Object.assign({}, a, b));
}
_bindWhole(choose_whole, func) {
const pat_val = this;
const query = function (state) {
@ -460,7 +463,7 @@ export class Pattern {
}
_compress(b, e) {
if (b > e || b > 1 || e > 1 || b < 0 || e < 0) {
if (b.gt(e) || b.gt(1) || e.gt(1) || b.lt(0) || e.lt(0)) {
return silence;
}
return this._fastGap(Fraction(1).div(e.sub(b)))._late(b);
@ -492,6 +495,13 @@ export class Pattern {
return this._squeezeBind(func);
}
_striate(n) {
const slices = Array.from({ length: n }, (x, i) => i);
const slice_objects = slices.map((i) => ({ begin: i / n, end: (i + 1) / n }));
const slicePat = slowcat(...slice_objects);
return this.set(slicePat)._fast(n);
}
// cpm = cycles per minute
_cpm(cpm) {
return this._fast(cpm / 60);
@ -554,7 +564,7 @@ export class Pattern {
}
_segment(rate) {
return this.struct(pure(true).fast(rate));
return this.struct(pure(true)._fast(rate));
}
invert() {
@ -611,6 +621,10 @@ export class Pattern {
return new Pattern(query)._splitQueries();
}
palindrome() {
return this.every(2, rev);
}
juxBy(by, func) {
by /= 2;
const elem_or = function (dict, key, dflt) {
@ -642,7 +656,7 @@ export class Pattern {
}
stutWith(times, time, func) {
return stack(...listRange(0, times - 1).map((i) => func(this.late(i * time), i)));
return stack(...listRange(0, times - 1).map((i) => func(this.late(Fraction(time).mul(i)), i)));
}
stut(times, feedback, time) {
@ -651,7 +665,7 @@ export class Pattern {
// these might change with: https://github.com/tidalcycles/Tidal/issues/902
_echoWith(times, time, func) {
return stack(...listRange(0, times - 1).map((i) => func(this.late(i * time), i)));
return stack(...listRange(0, times - 1).map((i) => func(this.late(Fraction(time).mul(i)), i)));
}
_echo(times, time, feedback) {
@ -713,6 +727,49 @@ export class Pattern {
}
}
// pattern composers
const composers = {
set: [
(a) => (b) => {
// If an object is involved, do a union, discarding matching keys from a.
// Otherwise, just return b.
if (a instanceof Object || b instanceof Object) {
if (!a instanceof Object) {
a = { value: a };
}
if (!b instanceof Object) {
b = { value: b };
}
return Object.assign({}, a, b);
}
return b;
},
id,
],
add: [(a) => (b) => a + b, (x) => x._asNumber()],
sub: [(a) => (b) => a - b, (x) => x._asNumber()],
mul: [(a) => (b) => a * b, (x) => x._asNumber()],
div: [(a) => (b) => a / b, (x) => x._asNumber()],
};
for (const [name, op] of Object.entries(composers)) {
Pattern.prototype[name] = function (...other) {
return op[1](this)._opLeft(sequence(other), op[0]);
};
Pattern.prototype[name + 'Flip'] = function (...other) {
return op[1](this)._opRight(sequence(other), op[0]);
};
Pattern.prototype[name + 'Sect'] = function (...other) {
return op[1](this)._opBoth(sequence(other), op[0]);
};
Pattern.prototype[name + 'Squeeze'] = function (...other) {
return op[1](this)._opSqueeze(sequence(other), op[0]);
};
Pattern.prototype[name + 'SqueezeFlip'] = function (...other) {
return op[1](this)._opSqueezeFlip(sequence(other), op[0]);
};
}
// methods of Pattern that get callable factories
Pattern.prototype.patternified = [
'apply',
@ -728,6 +785,7 @@ Pattern.prototype.patternified = [
'linger',
'ply',
'segment',
'striate',
'slow',
'velocity',
];
@ -774,7 +832,7 @@ export function slowcat(...pats) {
pats = pats.map(reify);
const query = function (state) {
const span = state.span;
const pat_n = Math.floor(span.begin) % pats.length;
const pat_n = mod(span.begin.sam(), pats.length);
const pat = pats[pat_n];
if (!pat) {
// pat_n can be negative, if the span is in the past..
@ -914,7 +972,7 @@ export const slow = curry((a, pat) => pat.slow(a));
export const struct = curry((a, pat) => pat.struct(a));
export const sub = curry((a, pat) => pat.sub(a));
export const superimpose = curry((array, pat) => pat.superimpose(...array));
export const union = curry((a, pat) => pat.union(a));
export const set = curry((a, pat) => pat.set(a));
export const when = curry((binary, f, pat) => pat.when(binary, f));
// problem: curried functions with spread arguments must have pat at the beginning

View File

@ -1,10 +1,11 @@
import { Hap } from './hap.mjs';
import { Pattern, fastcat } from './pattern.mjs';
import { Pattern, fastcat, reify, silence, stack } from './pattern.mjs';
import Fraction from './fraction.mjs';
import { id } from './util.mjs';
export function steady(value) {
// A continuous value
return new Pattern((span) => Hap(undefined, span, value));
return new Pattern((state) => [new Hap(undefined, state.span, value)]);
}
export const signal = (func) => {
@ -28,3 +29,181 @@ export const square2 = square._toBipolar();
export const tri = fastcat(isaw, saw);
export const tri2 = fastcat(isaw2, saw2);
export const time = signal(id);
// random signals
const xorwise = (x) => {
const a = (x << 13) ^ x;
const b = (a >> 17) ^ a;
return (b << 5) ^ b;
};
// stretch 300 cycles over the range of [0,2**29 == 536870912) then apply the xorshift algorithm
const _frac = (x) => x - Math.trunc(x);
const timeToIntSeed = (x) => xorwise(Math.trunc(_frac(x / 300) * 536870912));
const intSeedToRand = (x) => (x % 536870912) / 536870912;
const timeToRand = (x) => Math.abs(intSeedToRand(timeToIntSeed(x)));
const timeToRandsPrime = (seed, n) => {
const result = [];
for (let i = 0; i < n; ++n) {
result.push(intSeedToRand(seed));
seed = xorwise(seed);
}
return result;
};
const timeToRands = (t, n) => timeToRandsPrime(timeToIntSeed(t), n);
export const rand = signal(timeToRand);
export const _brandBy = (p) => rand.fmap((x) => x < p);
export const brandBy = (pPat) => reify(pPat).fmap(_brandBy).innerJoin();
export const brand = _brandBy(0.5);
export const _irand = (i) => rand.fmap((x) => Math.trunc(x * i));
export const irand = (ipat) => reify(ipat).fmap(_irand).innerJoin();
export const chooseWith = (pat, xs) => {
xs = xs.map(reify);
if (xs.length == 0) {
return silence;
}
return pat.range(0, xs.length).fmap((i) => xs[Math.floor(i)]).outerJoin();
};
export const choose = (...xs) => chooseWith(rand, xs);
const _wchooseWith = function (pat, ...pairs) {
const values = pairs.map((pair) => reify(pair[0]));
const weights = [];
let accum = 0;
for (const pair of pairs) {
accum += pair[1];
weights.push(accum);
}
const total = accum;
const match = function(r) {
const find = r * total;
return values[weights.findIndex((x) => x > find, weights)];
};
return pat.fmap(match);
};
const wchooseWith = (...args) => _wchooseWith(...args).outerJoin()
export const wchoose = (...pairs) => wchooseWith(rand, ...pairs);
export const wchooseCycles = (...pairs) => _wchooseWith(rand, ...pairs).innerJoin();
export const perlinWith = (pat) => {
const pata = pat.fmap(Math.floor);
const patb = pat.fmap((t) => Math.floor(t) + 1);
const smootherStep = (x) => 6.0 * x ** 5 - 15.0 * x ** 4 + 10.0 * x ** 3;
const interp = (x) => (a) => (b) => a + smootherStep(x) * (b - a);
return pat.sub(pata).fmap(interp).appBoth(pata.fmap(timeToRand)).appBoth(patb.fmap(timeToRand));
};
export const perlin = perlinWith(time);
Pattern.prototype._degradeByWith = function (withPat, x) {
return this.fmap((a) => (_) => a).appLeft(withPat._filterValues((v) => v > x));
};
Pattern.prototype._degradeBy = function (x) {
return this._degradeByWith(rand, x);
};
Pattern.prototype.degrade = function () {
return this._degradeBy(0.5);
};
Pattern.prototype._undegradeBy = function (x) {
return this._degradeByWith(
rand.fmap((r) => 1 - r),
x,
);
};
Pattern.prototype.undegrade = function () {
return this._undegradeBy(0.5);
};
Pattern.prototype._sometimesBy = function (x, func) {
return stack(this._degradeBy(x), func(this._undegradeBy(1 - x)));
};
Pattern.prototype.sometimesBy = function (patx, func) {
const pat = this;
return reify(patx)
.fmap((x) => pat._sometimesBy(x, func))
.innerJoin();
};
Pattern.prototype._sometimesByPre = function (x, func) {
return stack(this._degradeBy(x), func(this).undegradeBy(1 - x));
};
Pattern.prototype.sometimesByPre = function (patx, func) {
const pat = this;
return reify(patx)
.fmap((x) => pat._sometimesByPre(x, func))
.innerJoin();
};
Pattern.prototype.sometimes = function (func) {
return this._sometimesBy(0.5, func);
};
Pattern.prototype.sometimesPre = function (func) {
return this._sometimesByPre(0.5, func);
};
Pattern.prototype._someCyclesBy = function (x, func) {
return stack(
this._degradeByWith(rand._segment(1), x),
func(this._degradeByWith(rand.fmap((r) => 1 - r)._segment(1), 1 - x)),
);
};
Pattern.prototype.someCyclesBy = function (patx, func) {
const pat = this;
return reify(patx)
.fmap((x) => pat._someCyclesBy(x, func))
.innerJoin();
};
Pattern.prototype.someCycles = function (func) {
return this._someCyclesBy(0.5, func);
};
Pattern.prototype.often = function (func) {
return this.sometimesBy(0.75, func);
};
Pattern.prototype.rarely = function (func) {
return this.sometimesBy(0.25, func);
};
Pattern.prototype.almostNever = function (func) {
return this.sometimesBy(0.1, func);
};
Pattern.prototype.almostAlways = function (func) {
return this.sometimesBy(0.9, func);
};
Pattern.prototype.never = function (func) {
return this;
};
Pattern.prototype.always = function (func) {
return func(this);
};
Pattern.prototype.patternified.push('degradeBy', 'undegradeBy');

View File

@ -36,6 +36,9 @@ import {
id,
ply,
} from '../index.mjs';
import { steady } from '../signal.mjs';
//import { Time } from 'tone';
import pkg from 'tone';
const { Time } = pkg;
@ -47,6 +50,10 @@ const hap = (whole, part, value, context = {}) => new Hap(whole, part, value, co
const third = Fraction(1, 3);
const twothirds = Fraction(2, 3);
const sameFirst = (a, b) => {
return assert.deepStrictEqual(a._sortEventsByPart().firstCycle(), b._sortEventsByPart().firstCycle());
};
describe('TimeSpan', function () {
describe('equals()', function () {
it('Should be equal to the same value', function () {
@ -108,6 +115,18 @@ describe('Hap', function () {
assert.deepStrictEqual(state3, { incrementme: 12 });
});
});
describe('wholeOrPart()', () => {
const ts1 = new TimeSpan(Fraction(0), Fraction(1));
const ts0_5 = new TimeSpan(Fraction(0), Fraction(0.5));
const continuousHap = new Hap(undefined, ts1, 'hello');
const discreteHap = new Hap(ts1, ts0_5, 'hello');
it('Can pick a whole', () => {
assert.deepStrictEqual(discreteHap.wholeOrPart(), ts1);
});
it('Can pick a part', () => {
assert.deepStrictEqual(continuousHap.wholeOrPart(), ts1);
});
});
});
describe('Pattern', function () {
@ -131,6 +150,33 @@ describe('Pattern', function () {
assert.equal(pure(3).add(pure(4)).query(st(0, 1))[0].value, 7);
});
});
describe('addFlip()', () => {
it('Can add things with structure from second pattern', () => {
sameFirst(sequence(1, 2).addFlip(4), sequence(5, 6).struct(true));
});
});
describe('addSqueeze()', () => {
it('Can add while squeezing the second pattern inside the events of the first', () => {
sameFirst(
sequence(1, [2, 3]).addSqueeze(sequence(10, 20, 30)),
sequence(
[11, 21, 31],
[
[12, 22, 32],
[13, 23, 33],
],
),
);
});
});
describe('addSqueezeFlip()', () => {
it('Can add while squeezing the first pattern inside the events of the second', () => {
sameFirst(
sequence(1, [2, 3]).addSqueezeFlip(10, 20, 30),
sequence([11, [12, 13]], [21, [22, 23]], [31, [32, 33]]),
);
});
});
describe('sub()', function () {
it('Can subtract things', function () {
assert.equal(pure(3).sub(pure(4)).query(st(0, 1))[0].value, -1);
@ -146,14 +192,50 @@ describe('Pattern', function () {
assert.equal(pure(3).div(pure(2)).firstCycle()[0].value, 1.5);
});
});
describe('union()', function () {
it('Can union things', function () {
describe('set()', function () {
it('Can set things in objects', function () {
assert.deepStrictEqual(
pure({ a: 4, b: 6 })
.union(pure({ c: 7 }))
.set(pure({ c: 7 }))
.firstCycle()[0].value,
{ a: 4, b: 6, c: 7 },
);
sameFirst(
sequence({ a: 1, b: 2 }, { a: 2, b: 2 }, { a: 3, b: 2 }).set({ a: 4, c: 5 }),
sequence({ a: 4, b: 2, c: 5 }).fast(3),
);
});
it('Can set things with plain values', function () {
sameFirst(sequence(1, 2, 3).set(4), sequence(4).fast(3));
});
describe('setFlip()', () => {
it('Can set things with structure from second pattern', () => {
sameFirst(sequence(1, 2).setFlip(4), pure(4).mask(true, true));
});
});
describe('setSqueeze()', () => {
it('Can squeeze one pattern inside the events of another', () => {
sameFirst(
sequence(1, [2, 3]).setSqueeze(sequence('a', 'b', 'c')),
sequence(
['a', 'b', 'c'],
[
['a', 'b', 'c'],
['a', 'b', 'c'],
],
),
);
sameFirst(
sequence(1, [2, 3]).setSqueeze('a', 'b', 'c'),
sequence(
['a', 'b', 'c'],
[
['a', 'b', 'c'],
['a', 'b', 'c'],
],
),
);
});
});
});
describe('stack()', function () {
@ -292,6 +374,15 @@ describe('Pattern', function () {
);
});
});
describe('fastcat()', function () {
it('Can go into negative time', function () {
sameFirst(
fastcat('a','b','c')
.late(1000000),
fastcat('a','b','c'),
);
});
});
describe('slowcat()', function () {
it('Can concatenate things slowly', function () {
assert.deepStrictEqual(
@ -399,7 +490,7 @@ describe('Pattern', function () {
});
});
describe('struct()', function () {
it('Can restructure a pattern', function () {
it('Can restructure a discrete pattern', function () {
assert.deepStrictEqual(sequence('a', 'b').struct(sequence(true, true, true)).firstCycle(), [
hap(ts(0, third), ts(0, third), 'a'),
hap(ts(third, twothirds), ts(third, 0.5), 'a'),
@ -425,6 +516,12 @@ describe('Pattern', function () {
sequence('a', ['a', silence], 'a').firstCycle(),
);
});
it('Can structure a continuous pattern', () => {
assert.deepStrictEqual(
steady('a').struct(true, [true, true]).firstCycle(),
sequence('a', ['a', 'a']).firstCycle(),
);
});
});
describe('mask()', function () {
it('Can fragment a pattern', function () {
@ -616,6 +713,18 @@ describe('Pattern', function () {
sequence(pure('a').fast(3), [pure('b').fast(3), pure('c').fast(3)]).firstCycle(),
);
});
it('Doesnt drop events in the 9th cycle', () => {
// fixed with https://github.com/tidalcycles/strudel/commit/72eeaf446e3d5e186d63cc0d2276f0723cde017a
assert.equal(sequence(1, 2, 3).ply(2).early(8).firstCycle().length, 6);
});
});
describe('striate', () => {
it('Can striate(2)', () => {
sameFirst(
sequence({ sound: 'a' }).striate(2),
sequence({ sound: 'a', begin: 0, end: 0.5 }, { sound: 'a', begin: 0.5, end: 1 }),
);
});
});
describe('chop', () => {
it('Can _chop(2)', () => {

View File

@ -1,5 +1,6 @@
import { strict as assert } from 'assert';
import { isNote, tokenizeNote, toMidi, mod, compose } from '../util.mjs';
import { pure } from '../pattern.mjs';
import { isNote, tokenizeNote, toMidi, fromMidi, mod, compose, getFrequency } from '../util.mjs';
describe('isNote', () => {
it('should recognize notes without accidentals', () => {
@ -64,6 +65,21 @@ describe('toMidi', () => {
assert.equal(toMidi('C##3'), 50);
});
});
describe('fromMidi', () => {
it('should turn midi into frequency', () => {
assert.equal(fromMidi(69), 440);
assert.equal(fromMidi(57), 220);
});
});
describe('getFrequency', () => {
it('should turn midi into frequency', () => {
const happify = (val, context = {}) => pure(val).firstCycle()[0].setContext(context);
assert.equal(getFrequency(happify('a4')), 440);
assert.equal(getFrequency(happify('a3')), 220);
assert.equal(getFrequency(happify(440, { type: 'frequency' })), 440); // TODO: migrate when values are objects..
assert.equal(getFrequency(happify(432, { type: 'frequency' })), 432);
});
});
describe('mod', () => {
it('should work like regular modulo with positive numbers', () => {

View File

@ -33,7 +33,7 @@ export class TimeSpan {
// (Note that the output timespan probably does not start *at* Time 0 --
// that only happens when the input Arc starts at an integral Time.)
const b = this.begin.cyclePos();
const e = b + (this.end - this.begin);
const e = b.add(this.end.sub(this.begin));
return new TimeSpan(b, e);
}
@ -81,8 +81,7 @@ export class TimeSpan {
// Like 'sect', but raises an exception if the timespans don't intersect.
const result = this.intersection(other);
if (result == undefined) {
// TODO - raise exception
// raise ValueError(f'TimeSpan {self} and TimeSpan {other} do not intersect')
throw 'TimeSpans do not intersect';
}
return result;
}

View File

@ -27,7 +27,7 @@ export const fromMidi = (n) => {
// modulo that works with negative numbers e.g. mod(-1, 3) = 2
// const mod = (n: number, m: number): number => (n < 0 ? mod(n + m, m) : n % m);
export const mod = (n, m) => (n < 0 ? mod(n + m, m) : n % m);
export const mod = (n, m) => (n % m + m) % m;
export const getPlayableNoteValue = (event) => {
let { value: note, context } = event;
@ -40,6 +40,22 @@ export const getPlayableNoteValue = (event) => {
return note;
};
export const getFrequency = (event) => {
let { value, context } = event;
// 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 === 'number' && context.type !== 'frequency') {
value = fromMidi(event.value);
} else if (typeof value === 'string' && isNote(value)) {
value = fromMidi(toMidi(event.value));
} else if (typeof value !== 'number') {
throw new Error('not a note or frequency:' + value);
}
return value;
};
// rotate array by n steps (to the left)
export const rotate = (arr, n) => arr.slice(n).concat(arr.slice(0, n));
@ -60,6 +76,7 @@ export const removeUndefineds = (xs) => xs.filter((x) => x != undefined);
export const flatten = (arr) => [].concat(...arr);
export const id = (a) => a;
export const constant = (a, b) => a;
export const listRange = (min, max) => Array.from({ length: max - min + 1 }, (_, i) => i + min);

View File

@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/eval",
"version": "0.0.3",
"version": "0.0.5",
"lockfileVersion": 2,
"requires": true,
"packages": {

View File

@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/eval",
"version": "0.0.3",
"version": "0.0.5",
"description": "Code evaluator for strudel",
"main": "evaluate.mjs",
"directories": {
@ -28,7 +28,7 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "^0.0.3",
"@strudel.cycles/core": "^0.0.5",
"estraverse": "^5.3.0",
"shift-ast": "^6.1.0",
"shift-codegen": "^7.0.3",

View File

@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/midi",
"version": "0.0.4",
"version": "0.0.6",
"lockfileVersion": 2,
"requires": true,
"packages": {

View File

@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/midi",
"version": "0.0.4",
"version": "0.0.6",
"description": "Midi API for strudel",
"main": "midi.mjs",
"repository": {
@ -21,7 +21,7 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/tone": "^0.0.4",
"@strudel.cycles/tone": "^0.0.6",
"tone": "^14.7.77",
"webmidi": "^2.5.2"
}

View File

@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/mini",
"version": "0.0.4",
"version": "0.0.7",
"description": "Mini notation for strudel",
"main": "mini.mjs",
"type": "module",
@ -25,7 +25,8 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/eval": "^0.0.3",
"@strudel.cycles/tone": "^0.0.4"
"@strudel.cycles/core": "^0.0.5",
"@strudel.cycles/eval": "^0.0.5",
"@strudel.cycles/tone": "^0.0.6"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
import OSC from './osc.js';
import OSC from 'osc-js';
import { Pattern } from '@strudel.cycles/core';
const comm = new OSC();

View File

@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/osc",
"version": "0.0.1",
"version": "0.0.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
@ -9,24 +9,15 @@
"version": "0.0.1",
"license": "GPL-3.0-or-later",
"dependencies": {
"osc-js": "^2.3.0"
}
},
"node_modules/isomorphic-ws": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz",
"integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==",
"peerDependencies": {
"ws": "*"
"osc-js": "^2.3.2"
}
},
"node_modules/osc-js": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/osc-js/-/osc-js-2.3.0.tgz",
"integrity": "sha512-P2Oy9tf8Z9lQw8JZeR62HNqbKdxj7Kqbsag+ImiJvyxPDReGMVt5LtZbMh/7Ve/wbYEGODkQdFAaLHFVkIlHPw==",
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/osc-js/-/osc-js-2.3.2.tgz",
"integrity": "sha512-9i7J4u1hH+glooGMh+ki1ni0JGqKmylT8r0nXKugHbRK63rR+kl4O+5tGW6+/EszjbCju3KV+eXQQzFDdGrmhg==",
"dependencies": {
"isomorphic-ws": "4.0.1",
"ws": "8.5.0"
"ws": "^8.5.0"
}
},
"node_modules/ws": {
@ -51,19 +42,12 @@
}
},
"dependencies": {
"isomorphic-ws": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz",
"integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==",
"requires": {}
},
"osc-js": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/osc-js/-/osc-js-2.3.0.tgz",
"integrity": "sha512-P2Oy9tf8Z9lQw8JZeR62HNqbKdxj7Kqbsag+ImiJvyxPDReGMVt5LtZbMh/7Ve/wbYEGODkQdFAaLHFVkIlHPw==",
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/osc-js/-/osc-js-2.3.2.tgz",
"integrity": "sha512-9i7J4u1hH+glooGMh+ki1ni0JGqKmylT8r0nXKugHbRK63rR+kl4O+5tGW6+/EszjbCju3KV+eXQQzFDdGrmhg==",
"requires": {
"isomorphic-ws": "4.0.1",
"ws": "8.5.0"
"ws": "^8.5.0"
}
},
"ws": {

View File

@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/osc",
"version": "0.0.1",
"version": "0.0.2",
"description": "OSC messaging for strudel",
"main": "osc.mjs",
"scripts": {
@ -21,12 +21,13 @@
"algorave"
],
"author": "Felix Roos <flix91@gmail.com>",
"contributors": ["Alex McLean <alex@slab.org>"],
"license": "GPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"osc-js": "^2.3.0"
"osc-js": "^2.3.2"
}
}

View File

@ -0,0 +1,4 @@
# @strudel.cycles/serial
This package adds webserial functionality to strudel Patterns, for e.g. sending messages to arduino microcontrollers.

View File

@ -0,0 +1,24 @@
{
"name": "@strudel.cycles/serial",
"version": "0.0.6",
"description": "Webserial API for strudel",
"main": "serial.mjs",
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
},
"keywords": [
"titdalcycles",
"strudel",
"pattern",
"livecoding",
"algorave"
],
"author": "Alex McLean <alex@slab.org>",
"license": "GPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {}
}

View File

@ -0,0 +1,52 @@
import { Pattern, isPattern } from '@strudel.cycles/core';
var serialWriter;
var choosing = false;
export async function getWriter(br=115200) {
if (choosing) {
return;
}
choosing = true;
if (serialWriter) {
return serialWriter;
}
if ('serial' in navigator) {
const port = await navigator.serial.requestPort();
await port.open({ baudRate: br });
const textEncoder = new TextEncoderStream();
const writableStreamClosed = textEncoder.readable.pipeTo(port.writable);
const writer = textEncoder.writable.getWriter();
serialWriter = function (message) {
writer.write(message)
}
}
else {
throw('Webserial is not available in this browser.')
}
}
const latency = 0.1;
// Pattern.prototype.midi = function (output: string | number, channel = 1) {
Pattern.prototype.serial = async function (...args) {
return this._withEvent((event) => {
if (!serialWriter) {
getWriter(...args);
}
const onTrigger = (time, event, currentTime) => {
var message = "";
if (typeof event.value === 'object') {
for (const [key, val] of Object.entries(event.value).flat()) {
message += `${key}:${val};`
}
}
else {
message = event.value;
}
const offset = (time - currentTime + latency) * 1000;
window.setTimeout(serialWriter, offset, message);
};
return event.setContext({ ...event.context, onTrigger });
});
};

View File

@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/tonal",
"version": "0.0.3",
"version": "0.0.5",
"lockfileVersion": 2,
"requires": true,
"packages": {

View File

@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/tonal",
"version": "0.0.3",
"version": "0.0.5",
"description": "Tonal functions for strudel",
"main": "tonal.mjs",
"type": "module",
@ -25,7 +25,7 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "^0.0.3",
"@strudel.cycles/core": "^0.0.5",
"@tonaljs/tonal": "^4.6.5",
"webmidi": "^3.0.15"
}

View File

@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/tone",
"version": "0.0.4",
"version": "0.0.6",
"lockfileVersion": 2,
"requires": true,
"packages": {

View File

@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/tone",
"version": "0.0.4",
"version": "0.0.6",
"description": "Tone.js API for strudel",
"main": "tone.mjs",
"type": "module",
@ -22,7 +22,7 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "^0.0.3",
"@strudel.cycles/core": "^0.0.5",
"@tonejs/piano": "^0.2.1",
"chord-voicings": "^0.0.1",
"tone": "^14.7.77"

View File

@ -117,157 +117,3 @@ export const highpass = (v) => new Filter(v, 'highpass');
export const adsr = (a, d = 0.1, s = 0.4, r = 0.01) => ({ envelope: { attack: a, decay: d, sustain: s, release: r } });
export const osc = (type) => ({ oscillator: { type } });
export const out = () => getDestination();
/*
You are entering experimental zone
*/
// the following code is an attempt to minimize tonejs code.. it is still an experiment
const chainable = function (instr) {
const _chain = instr.chain.bind(instr);
let chained = [];
instr.chain = (...args) => {
chained = chained.concat(args);
instr.disconnect(); // disconnect from destination / previous chain
return _chain(...chained, getDestination());
};
// shortcuts: chaining multiple won't work forn now.. like filter(1000).gain(0.5). use chain + native Tone calls instead
// instr.filter = (freq = 1000, type: BiquadFilterType = 'lowpass') =>
instr.filter = (freq = 1000, type = 'lowpass') =>
instr.chain(
new Filter(freq, type), // .Q.setValueAtTime(q, time);
);
instr.gain = (gain = 0.9) => instr.chain(new Gain(gain));
return instr;
};
// helpers
export const poly = (type) => {
const s = new PolySynth(Synth, { oscillator: { type } }).toDestination();
return chainable(s);
};
Pattern.prototype._poly = function (type = 'triangle') {
const instrumentConfig = {
oscillator: { type },
envelope: { attack: 0.01, decay: 0.01, sustain: 0.6, release: 0.01 },
};
if (!this.instrument) {
// create only once to keep the js heap happy
// this.instrument = new PolySynth(Synth, instrumentConfig).toDestination();
this.instrument = poly(type);
}
return this._withEvent((event) => {
const onTrigger = (time, event) => {
this.instrument.set(instrumentConfig);
this.instrument.triggerAttackRelease(event.value, event.duration, time);
};
return event.setContext({ ...event.context, instrumentConfig, onTrigger });
});
};
Pattern.prototype.define('poly', (type, pat) => pat.poly(type), { composable: true, patternified: true });
/*
You are entering danger zone
*/
// everything below is nice in theory, but not healthy for the JS heap, as nodes get recreated on every call
const getTrigger = (getChain, value) => (time, event) => {
const chain = getChain(); // make sure this returns a node that is connected toDestination // time
if (!isNote(value)) {
throw new Error('not a note: ' + value);
}
chain.triggerAttackRelease(value, event.duration, time);
setTimeout(() => {
// setTimeout is a little bit better compared to Transport.scheduleOnce
chain.dispose(); // mark for garbage collection
}, event.duration * 2000);
};
Pattern.prototype._synth = function (type = 'triangle') {
return this._withEvent((event) => {
const instrumentConfig = {
oscillator: { type },
envelope: { attack: 0.01, decay: 0.01, sustain: 0.6, release: 0.01 },
};
const getInstrument = () => {
const instrument = new Synth();
instrument.set(instrumentConfig);
return instrument;
};
const onTrigger = getTrigger(() => getInstrument().toDestination(), event.value);
return event.setContext({ ...event.context, getInstrument, instrumentConfig, onTrigger });
});
};
Pattern.prototype.adsr = function (attack = 0.01, decay = 0.01, sustain = 0.6, release = 0.01) {
return this._withEvent((event) => {
if (!event.context.getInstrument) {
throw new Error('cannot chain adsr: need instrument first (like synth)');
}
const instrumentConfig = { ...event.context.instrumentConfig, envelope: { attack, decay, sustain, release } };
const getInstrument = () => {
const instrument = event.context.getInstrument();
instrument.set(instrumentConfig);
return instrument;
};
const onTrigger = getTrigger(() => getInstrument().toDestination(), event.value);
return event.setContext({ ...event.context, getInstrument, instrumentConfig, onTrigger });
});
};
Pattern.prototype.chain = function (...effectGetters) {
return this._withEvent((event) => {
if (!event.context?.getInstrument) {
throw new Error('cannot chain: need instrument first (like synth)');
}
const chain = (event.context.chain || []).concat(effectGetters);
const getChain = () => {
const effects = chain.map((getEffect) => getEffect());
return event.context.getInstrument().chain(...effects, getDestination());
};
const onTrigger = getTrigger(getChain, event.value);
return event.setContext({ ...event.context, getChain, onTrigger, chain });
});
};
export const autofilter =
(freq = 1) =>
() =>
new AutoFilter(freq).start();
export const filter =
// (freq = 1, q = 1, type: BiquadFilterType = 'lowpass') =>
(freq = 1, q = 1, type = 'lowpass') =>
() =>
new Filter(freq, type); // .Q.setValueAtTime(q, time);
export const gain =
(gain = 0.9) =>
() =>
new Gain(gain);
Pattern.prototype._gain = function (g) {
return this.chain(gain(g));
};
// Pattern.prototype._filter = function (freq: number, q: number, type: BiquadFilterType = 'lowpass') {
Pattern.prototype._filter = function (freq, q, type = 'lowpass') {
return this.chain(filter(freq, q, type));
};
Pattern.prototype._autofilter = function (g) {
return this.chain(autofilter(g));
};
Pattern.prototype.define('synth', (type, pat) => pat.synth(type), { composable: true, patternified: true });
Pattern.prototype.define('gain', (gain, pat) => pat.synth(gain), { composable: true, patternified: true });
Pattern.prototype.define('filter', (cutoff, pat) => pat.filter(cutoff), { composable: true, patternified: true });
Pattern.prototype.define('autofilter', (cutoff, pat) => pat.filter(cutoff), { composable: true, patternified: true });

View File

@ -0,0 +1,29 @@
# @strudel.cycles/webaudio
This package contains a scheduler + a clockworker and synths based on the Web Audio API.
It's an alternative to `@strudel.cycles/tone`, with better performance, but less features.
## Install
```sh
npm i @strudel.cycles/webaudio --save
```
## Example
```js
import { Scheduler, getAudioContext } from '@strudel.cycles/webaudio';
const scheduler = new Scheduler({
audioContext: getAudioContext(),
interval: 0.1,
onEvent: (e) => e.context?.createAudioNode?.(e),
});
const pattern = sequence([55, 99], 110).osc('sawtooth').out()
scheduler.setPattern(pattern);
scheduler.start()
//scheduler.stop()
```
A more sophisticated example can be found in [examples/repl.html](./examples/repl.html).
You can run it by opening the html file with your browser, or by clicking [this link](https://raw.githack.com/tidalcycles/strudel/main/packages/webaudio/examples/repl.html)

View File

@ -0,0 +1,69 @@
// 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 class is basically the tale of two clocks
class ClockWorker {
worker;
audioContext;
interval = 0.2; // query span
lastEnd = 0;
constructor(audioContext, callback, interval = this.interval) {
this.audioContext = audioContext;
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.');
}
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;
const precision = 100;
this.worker.onmessage = (e) => {
if (e.data === 'tick') {
const begin = this.lastEnd || this.audioContext.currentTime;
const end = this.audioContext.currentTime + this.interval; // DONT reference begin here!
this.lastEnd = end;
// callback with query span, using clock #2 (the audio clock)
callback(begin, end);
}
};
}
start() {
console.log('start...');
this.audioContext.resume();
this.worker.postMessage('start');
}
stop() {
console.log('stop...');
this.worker.postMessage('stop');
}
}
export default ClockWorker;

View File

@ -0,0 +1,64 @@
<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="font-size: 2em">stop</button>
</div>
<textarea
style="font-size: 2em; background: #bce865; color: #323230; 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 strudel from '../../core/index.mjs';
import * as util from '../../core/util.mjs';
import '@strudel.cycles/core/euclid.mjs';
// import { Scheduler, getAudioContext } from 'https://cdn.skypack.dev/@strudel.cycles/webaudio@0.0.4';
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)
.velocity(.5)
.wave(cat('sawtooth','square').fast(2))
.adsr(0.01,.02,.5,0.1)
.filter('lowshelf',800,25)
.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());
</script>

View File

@ -0,0 +1,3 @@
export { default as ClockWorker } from './clockworker.mjs';
export { default as Scheduler } from './scheduler.mjs';
export * from './webaudio.mjs';

3851
packages/webaudio/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,32 @@
{
"name": "@strudel.cycles/webaudio",
"version": "0.0.6",
"description": "Web Audio helpers for Strudel",
"main": "index.mjs",
"directories": {
"example": "examples"
},
"scripts": {
"example": "npx parcel examples/repl.html"
},
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
},
"keywords": [
"tidalcycles",
"strudel",
"pattern",
"livecoding",
"algorave"
],
"author": "Felix Roos <flix91@gmail.com>",
"license": "GPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "^0.0.5"
}
}

View File

@ -0,0 +1,39 @@
import ClockWorker from './clockworker.mjs';
import { State, TimeSpan } from '@strudel.cycles/core';
class Scheduler {
worker;
pattern;
constructor({ audioContext, interval = 0.2, onEvent }) {
this.worker = new ClockWorker(
audioContext,
(begin, end) => {
this.pattern.query(new State(new TimeSpan(begin, end))).forEach((e) => {
if (!e.part.begin.equals(e.whole.begin)) {
return;
}
if (onEvent) {
onEvent?.(e);
} else {
console.warn('unplayable event: no audio node nor onEvent callback', e);
}
});
},
interval,
);
}
start() {
if (!this.pattern) {
throw new Error('Scheduler: no pattern set! call .setPattern first.');
}
this.worker.start();
}
stop() {
this.worker.stop();
}
setPattern(pat) {
this.pattern = pat;
}
}
export default Scheduler;

View File

@ -0,0 +1,87 @@
import { Pattern, getFrequency, patternify2 } from '@strudel.cycles/core';
import { Tone } from '@strudel.cycles/tone';
// let audioContext;
export const getAudioContext = () => {
return Tone.getContext().rawContext;
/* if (!audioContext) {
audioContext = new AudioContext();
}
return audioContext; */
};
const lookahead = 0.2;
const adsr = (attack, decay, sustain, release, velocity, begin, end) => {
const gainNode = getAudioContext().createGain();
gainNode.gain.setValueAtTime(0, begin);
gainNode.gain.linearRampToValueAtTime(velocity, begin + attack); // attack
gainNode.gain.linearRampToValueAtTime(sustain * velocity, begin + attack + decay); // sustain start
gainNode.gain.setValueAtTime(sustain * velocity, end); // sustain end
gainNode.gain.linearRampToValueAtTime(0, end + release); // release
// for some reason, using exponential ramping creates little cracklings
return gainNode;
};
Pattern.prototype.withAudioNode = function (createAudioNode) {
return this._withEvent((event) => {
return event.setContext({
...event.context,
createAudioNode: (t, e) => createAudioNode(t, e, event.context.createAudioNode?.(t, event)),
});
});
};
Pattern.prototype._wave = function (type) {
return this.withAudioNode((t, e) => {
const osc = getAudioContext().createOscillator();
osc.type = type;
const f = getFrequency(e);
osc.frequency.value = f; // expects frequency..
const begin = t ?? e.whole.begin.valueOf() + lookahead;
const end = begin + e.duration;
osc.start(begin);
osc.stop(end); // release?
return osc;
});
};
Pattern.prototype.adsr = function (a = 0.01, d = 0.05, s = 1, r = 0.01) {
return this.withAudioNode((t, e, node) => {
const velocity = e.context?.velocity || 1;
const begin = t ?? e.whole.begin.valueOf() + lookahead;
const end = begin + e.duration + lookahead;
const envelope = adsr(a, d, s, r, velocity, begin, end);
node?.connect(envelope);
return envelope;
});
};
Pattern.prototype._filter = function (type = 'lowpass', frequency = 1000) {
return this.withAudioNode((t, e, node) => {
const filter = getAudioContext().createBiquadFilter();
filter.type = type;
filter.frequency.value = frequency;
node?.connect(filter);
return filter;
});
};
Pattern.prototype.filter = function (type, frequency) {
return patternify2(Pattern.prototype._filter)(reify(type), reify(frequency), this);
};
Pattern.prototype.out = function () {
const master = getAudioContext().createGain();
master.gain.value = 0.1;
master.connect(getAudioContext().destination);
return this.withAudioNode((t, e, node) => {
if (!node) {
console.warn('out: no source! call .osc() first');
}
node?.connect(master);
})._withEvent((event) => {
const onTrigger = (time, e) => e.context?.createAudioNode?.(time, e);
return event.setContext({ ...event.context, onTrigger });
});
};
Pattern.prototype.define('wave', (type, pat) => pat.wave(type), { patternified: true });

View File

@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/xen",
"version": "0.0.3",
"version": "0.0.5",
"description": "Xenharmonic API for strudel",
"main": "xen.mjs",
"scripts": {
@ -24,6 +24,6 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "^0.0.3"
"@strudel.cycles/core": "^0.0.5"
}
}

View File

@ -13,11 +13,8 @@ The REPL is deployed at [strudel.tidalcycles.org](https://strudel.tidalcycles.or
```bash
# from project root
npm install
npx lerna bootstrap
cd repl
npm install
npm run start
npm run setup
npm run repl
```
## Build REPL
@ -27,7 +24,3 @@ cd repl
npm run build # <- builds repl + tutorial to ../docs
npm run static # <- test static build
```
## Dev Notes
~~Always run `npm i --legacy-peer-deps`, otherwise `@tonejs/piano` will break.~~

View File

@ -30,6 +30,8 @@ import '@strudel.cycles/core/speak.mjs';
import '@strudel.cycles/tone/pianoroll.mjs';
import '@strudel.cycles/tone/draw.mjs';
import '@strudel.cycles/osc/osc.mjs';
import '@strudel.cycles/webaudio/webaudio.mjs';
import '@strudel.cycles/serial/serial.mjs';
import controls from '@strudel.cycles/core/controls.mjs';
extend(

View File

@ -703,3 +703,124 @@ stack(
>\`
.legato(.5)
).fast(2) //.tone((await piano()).chain(out()))`;
export const speakerman = `backgroundImage('https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fi.ytimg.com%2Fvi%2FXR0rKqW3VwY%2Fmaxresdefault.jpg&f=1&nofb=1',
{ className:'darken', style:'background-size:cover'})
stack(
"[g3,bb3,d4] [f3,a3,c4] [c3,e3,g3]@2".slow(2).late(.1),
slowcat(
'Baker man',
'is baking bread',
'Baker man',
'is baking bread',
'Sagabona',
'kunjani wena',
'Sagabona',
'kunjani wena',
'The night train, is coming',
'got to keep on running',
'The night train, is coming',
'got to keep on running',
).speak("en zu en".slow(12), "<0 2 3 4 5 6>".slow(2)),
).slow(4)`;
export const randomBells = `const delay = new FeedbackDelay(1/3, .8).chain(vol(.2), out());
let bell = await sampler({
C6: 'https://freesound.org/data/previews/411/411089_5121236-lq.mp3'
})
const bass = await sampler({
d2: 'https://freesound.org/data/previews/608/608286_13074022-lq.mp3'
});
bell = bell.chain(vol(0.6).connect(delay),out());
"0".euclidLegato(3,8)
.echo(3, 1/16, .5)
.add(rand.range(0,12))
.velocity(rand.range(.5,1))
.legato(rand.range(.4,3))
.scale(slowcat('D minor pentatonic')).tone(bell)
.stack("<D2 A2 G2 F2>".euclidLegato(6,8,1).tone(bass.toDestination()))
.slow(6)
.pianoroll({minMidi:20,maxMidi:120,background:'transparent'})`;
export const waa = `"a4 [a3 c3] a3 c3"
.sub("<7 12>/2")
.off(1/8, add("12"))
.off(1/4, add("7"))
.legato(.5)
.slow(2)
.wave("sawtooth square")
.filter('lowpass', "<2000 1000 500>")
.out()`;
export const waar = `"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))
.out()
.pianoroll({minMidi:20,maxMidi:120,background:'#202124'})`;
export const hyperpop = `const lfo = cosine.slow(15);
const lfo2 = sine.slow(16);
const filter1 = x=>x.filter('lowpass', lfo2.range(300,3000));
const filter2 = x=>x.filter('highpass', lfo.range(1000,6000)).filter('lowpass',4000)
const scales = slowcat('D3 major', 'G3 major').slow(8)
const drums = await players({
bd: '344/344757_1676145-lq.mp3',
sn: '387/387186_7255534-lq.mp3',
hh: '561/561241_12517458-lq.mp3',
hh2:'44/44944_236326-lq.mp3',
hh3: '44/44944_236326-lq.mp3',
}, 'https://freesound.org/data/previews/')
stack(
"-7 0 -7 7".struct("x(5,8,2)").fast(2).sub(7)
.scale(scales).wave("sawtooth,square").velocity(.3).adsr(0.01,0.1,.5,0)
.apply(filter1),
"~@3 [<2 3>,<4 5>]"
.echo(8,1/16,.7)
.scale(scales)
.wave('square').velocity(.7).adsr(0.01,0.1,0).apply(filter1),
"6 5 4".add(14)
.superimpose(sub("5"))
.fast(1).euclidLegato(3,8)
.mask("<1 0@7>")
.fast(2)
.echo(32, 1/8, .9)
.scale(scales)
.wave("sawtooth")
.velocity(.2)
.adsr(.01,.5,0)
.apply(filter2)
//.echo(4,1/16,.5)
).out().stack(
stack(
"bd <~@7 [~ bd]>".fast(2),
"~ sn",
"[~ hh3]*2"
).tone(drums.chain(vol(.18),out())).fast(2)
).slow(2)
//.pianoroll({minMidi:20, maxMidi:160})
// strudel disable-highlighting`;
export const festivalOfFingers3 = `"[-7*3],0,2,6,[8 7]"
.echoWith(4,1/4, (x,n)=>x
.add(n*7)
.velocity(1/(n+1))
.legato(1/(n+1)))
.velocity(perlin.range(.5,.9).slow(8))
.stack("[22 25]*3"
.legato(sine.range(.5,2).slow(8))
.velocity(sine.range(.4,.8).slow(5))
.echo(4,1/12,.5))
.scale(slowcat('D dorian','G mixolydian','C dorian','F mixolydian'))
.legato(1)
.slow(2)
.tone((await piano()).toDestination())
//.pianoroll({maxMidi:160})`;

View File

@ -195,14 +195,14 @@ Internally, the mini notation will expand to use the actual functional JavaScrip
Notes are automatically available as variables:
<MiniRepl tune={`e4`} />
<MiniRepl tune={`sequence(d4, fs4, a4)`} />
An important difference to the mini notation:
For sharp notes, the letter "s" is used instead of "#", because JavaScript does not support "#" in a variable name.
The above is the same as:
<MiniRepl tune={`"e4"`} />
<MiniRepl tune={`sequence('d4', 'f#4', 'a4')`} />
Using strings, you can also use "#".
@ -590,59 +590,6 @@ Helper to set the envelope of a Tone.js instrument. Intended to be used with Ton
.tone(synth(adsr(0,.1,0,0)).chain(out()))`}
/>
### Experimental: Patternification
While the above methods work for static sounds, there is also the option to patternify tone methods.
This is currently experimental, because the performance is not stable, and audio glitches will appear after some time.
It would be great to get this to work without glitches though, because it is fun!
#### synth(type)
With .synth, you can create a synth with a variable wave type:
<MiniRepl
tune={`"[c4 c4 bb3 c4] [~ g3 ~ g3] [c4 f4 e4 c4] ~"
.synth("<sawtooth8 square8>").slow(4)`}
/>
#### adsr(attack, decay?, sustain?, release?)
Chainable Envelope helper:
<MiniRepl
tune={`"[c5 c5 bb4 c5] [~ g4 ~ g4] [c5 f5 e5 c5] ~".slow(4)
.synth('sawtooth16').adsr(0,.1,0,0)`}
/>
Due to having more than one argument, this method is not patternified.
#### filter(cuttoff)
Patternified filter:
<MiniRepl
tune={`"[c4 c4 bb3 c4] [~ g3 ~ g3] [c4 f4 e4 c4] ~"
.synth('sawtooth16').filter("[500 2000]*8").slow(4)`}
/>
#### gain(value)
Patternified gain:
<MiniRepl
tune={`"[c4 c4 bb3 c4] [~ g3 ~ g3] [c4 f4 e4 c4] ~"
.synth('sawtooth16').gain("[.2 .8]*8").slow(4)`}
/>
#### autofilter(value)
Patternified autofilter:
<MiniRepl
tune={`"c2 c3"
.synth('sawtooth16').autofilter("<1 4 8>")`}
/>
## Tonal API
The Tonal API, uses [tonaljs](https://github.com/tonaljs/tonal) to provide helpers for musical operations.

View File

@ -39,7 +39,7 @@ function useCycle(props) {
// schedule events for next cycle
events
?.filter((event) => event.part.begin.equals(event.whole.begin))
?.filter((event) => event.part.begin.equals(event.whole?.begin))
.forEach((event) => {
Tone.getTransport().schedule((time) => {
onEvent(time, event, Tone.getContext().currentTime);

View File

@ -1,21 +0,0 @@
// Snowpack Configuration File
// See all supported options: https://www.snowpack.dev/reference/configuration
/** @type {import("snowpack").SnowpackUserConfig } */
module.exports = {
mount: {
/* ... */
},
plugins: [
/* ... */
],
packageOptions: {
/* ... */
},
devOptions: {
/* ... */
},
buildOptions: {
/* ... */
},
};