Merge branch 'main' into jade/uwu2000

This commit is contained in:
Jade (Rose) Rowland 2025-02-24 20:12:12 -05:00
commit d9c3f051b9
17 changed files with 528 additions and 141 deletions

View file

@ -31,6 +31,10 @@ This project is organized into many [packages](./packages), which are also avail
Read more about how to use these in your own project [here](https://strudel.cc/technical-manual/project-start). Read more about how to use these in your own project [here](https://strudel.cc/technical-manual/project-start).
You will need to abide by the terms of the [GNU Affero Public Licence v3](LICENSE.md). As such, Strudel code can only be shared within free/open source projects under the same license -- see the license for details.
Licensing info for the default sound banks can be found over on the [dough-samples](https://github.com/felixroos/dough-samples/blob/main/README.md) repository.
## Contributing ## Contributing
There are many ways to contribute to this project! See [contribution guide](./CONTRIBUTING.md). There are many ways to contribute to this project! See [contribution guide](./CONTRIBUTING.md).

View file

@ -59,6 +59,7 @@
"@eslint/eslintrc": "^3.2.0", "@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.19.0", "@eslint/js": "^9.19.0",
"@tauri-apps/cli": "^2.2.7", "@tauri-apps/cli": "^2.2.7",
"@vitest/coverage-v8": "3.0.4",
"@vitest/ui": "^3.0.4", "@vitest/ui": "^3.0.4",
"acorn": "^8.14.0", "acorn": "^8.14.0",
"dependency-tree": "^11.0.1", "dependency-tree": "^11.0.1",

View file

@ -1244,6 +1244,16 @@ export function reify(thing) {
return pure(thing); return pure(thing);
} }
/** Takes a list of patterns, and returns a pattern of lists.
*/
export function sequenceP(pats) {
let result = pure([]);
for (const pat of pats) {
result = result.bind((list) => pat.fmap((v) => list.concat([v])));
}
return result;
}
/** The given items are played at the same time at the same length. /** The given items are played at the same time at the same length.
* *
* @return {Pattern} * @return {Pattern}

View file

@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
*/ */
import { Hap } from './hap.mjs'; import { Hap } from './hap.mjs';
import { Pattern, fastcat, pure, register, reify, silence, stack } from './pattern.mjs'; import { Pattern, fastcat, pure, register, reify, silence, stack, sequenceP } from './pattern.mjs';
import Fraction from './fraction.mjs'; import Fraction from './fraction.mjs';
import { id, keyAlias, getCurrentKeyboardState } from './util.mjs'; import { id, keyAlias, getCurrentKeyboardState } from './util.mjs';
@ -433,19 +433,30 @@ export const chooseCycles = (...xs) => chooseInWith(rand.segment(1), xs);
export const randcat = chooseCycles; export const randcat = chooseCycles;
const _wchooseWith = function (pat, ...pairs) { const _wchooseWith = function (pat, ...pairs) {
// A list of patterns of values
const values = pairs.map((pair) => reify(pair[0])); const values = pairs.map((pair) => reify(pair[0]));
// A list of weight patterns
const weights = []; const weights = [];
let accum = 0;
let total = pure(0);
for (const pair of pairs) { for (const pair of pairs) {
accum += pair[1]; // 'add' accepts either values or patterns of values here, so no need
weights.push(accum); // to explicitly reify
total = total.add(pair[1]);
// accumulate our list of weight patterns
weights.push(total);
} }
const total = accum; // a pattern of lists of weights
const weightspat = sequenceP(weights);
// Takes a number from 0-1, returns a pattern of patterns of values
const match = function (r) { const match = function (r) {
const find = r * total; const findpat = total.mul(r);
return values[weights.findIndex((x) => x > find, weights)]; return weightspat.fmap((weights) => (find) => values[weights.findIndex((x) => x > find, weights)]).appLeft(findpat);
}; };
return pat.fmap(match); // This returns a pattern of patterns.. The innerJoin is in wchooseCycles
return pat.bind(match);
}; };
const wchooseWith = (...args) => _wchooseWith(...args).outerJoin(); const wchooseWith = (...args) => _wchooseWith(...args).outerJoin();
@ -467,6 +478,9 @@ export const wchoose = (...pairs) => wchooseWith(rand, ...pairs);
* wchooseCycles(["bd",10], ["hh",1], ["sd",1]).s().fast(8) * wchooseCycles(["bd",10], ["hh",1], ["sd",1]).s().fast(8)
* @example * @example
* wchooseCycles(["bd bd bd",5], ["hh hh hh",3], ["sd sd sd",1]).fast(4).s() * wchooseCycles(["bd bd bd",5], ["hh hh hh",3], ["sd sd sd",1]).fast(4).s()
* @example
* // The probability can itself be a pattern
* wchooseCycles(["bd(3,8)","<5 0>"], ["hh hh hh",3]).fast(4).s()
*/ */
export const wchooseCycles = (...pairs) => _wchooseWith(rand.segment(1), ...pairs).innerJoin(); export const wchooseCycles = (...pairs) => _wchooseWith(rand.segment(1), ...pairs).innerJoin();

95
pnpm-lock.yaml generated
View file

@ -39,6 +39,9 @@ importers:
'@tauri-apps/cli': '@tauri-apps/cli':
specifier: ^2.2.7 specifier: ^2.2.7
version: 2.2.7 version: 2.2.7
'@vitest/coverage-v8':
specifier: 3.0.4
version: 3.0.4(vitest@3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0))
'@vitest/ui': '@vitest/ui':
specifier: ^3.0.4 specifier: ^3.0.4
version: 3.0.4(vitest@3.0.4) version: 3.0.4(vitest@3.0.4)
@ -1412,6 +1415,10 @@ packages:
resolution: {integrity: sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg==} resolution: {integrity: sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
'@bcoe/v8-coverage@1.0.2':
resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
engines: {node: '>=18'}
'@codemirror/autocomplete@6.18.4': '@codemirror/autocomplete@6.18.4':
resolution: {integrity: sha512-sFAphGQIqyQZfP2ZBsSHV7xQvo9Py0rV0dW7W3IMRdS+zDuNb2l3no78CvUaWKGfzFjI4FTrLdUSj86IGb2hRA==} resolution: {integrity: sha512-sFAphGQIqyQZfP2ZBsSHV7xQvo9Py0rV0dW7W3IMRdS+zDuNb2l3no78CvUaWKGfzFjI4FTrLdUSj86IGb2hRA==}
@ -1834,6 +1841,10 @@ packages:
'@isaacs/string-locale-compare@1.1.0': '@isaacs/string-locale-compare@1.1.0':
resolution: {integrity: sha512-SQ7Kzhh9+D+ZW9MA0zkYv3VXhIDNx+LzM6EJ+/65I3QY+enU6Itte7E5XX7EWrqLW2FN4n06GWzBnPoC3th2aQ==} resolution: {integrity: sha512-SQ7Kzhh9+D+ZW9MA0zkYv3VXhIDNx+LzM6EJ+/65I3QY+enU6Itte7E5XX7EWrqLW2FN4n06GWzBnPoC3th2aQ==}
'@istanbuljs/schema@0.1.3':
resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==}
engines: {node: '>=8'}
'@jest/schemas@29.6.3': '@jest/schemas@29.6.3':
resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@ -2818,6 +2829,15 @@ packages:
peerDependencies: peerDependencies:
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 vite: ^4.2.0 || ^5.0.0 || ^6.0.0
'@vitest/coverage-v8@3.0.4':
resolution: {integrity: sha512-f0twgRCHgbs24Dp8cLWagzcObXMcuKtAwgxjJV/nnysPAJJk1JiKu/W0gIehZLmkljhJXU/E0/dmuQzsA/4jhA==}
peerDependencies:
'@vitest/browser': 3.0.4
vitest: 3.0.4
peerDependenciesMeta:
'@vitest/browser':
optional: true
'@vitest/expect@3.0.4': '@vitest/expect@3.0.4':
resolution: {integrity: sha512-Nm5kJmYw6P2BxhJPkO3eKKhGYKRsnqJqf+r0yOGRKpEP+bSCBDsjXgiu1/5QFrnPMEgzfC38ZEjvCFgaNBC0Eg==} resolution: {integrity: sha512-Nm5kJmYw6P2BxhJPkO3eKKhGYKRsnqJqf+r0yOGRKpEP+bSCBDsjXgiu1/5QFrnPMEgzfC38ZEjvCFgaNBC0Eg==}
@ -4487,6 +4507,9 @@ packages:
hs2js@0.1.0: hs2js@0.1.0:
resolution: {integrity: sha512-THlUIMX8tZf6gtbz5RUZ8xQUyKJEItsx7bxEBcouFIEWjeo90376WMocj3JEz6qTv5nM+tjo3vNvLf89XruMvg==} resolution: {integrity: sha512-THlUIMX8tZf6gtbz5RUZ8xQUyKJEItsx7bxEBcouFIEWjeo90376WMocj3JEz6qTv5nM+tjo3vNvLf89XruMvg==}
html-escaper@2.0.2:
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
html-escaper@3.0.3: html-escaper@3.0.3:
resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==} resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==}
@ -4837,6 +4860,22 @@ packages:
resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
istanbul-lib-coverage@3.2.2:
resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==}
engines: {node: '>=8'}
istanbul-lib-report@3.0.1:
resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==}
engines: {node: '>=10'}
istanbul-lib-source-maps@5.0.6:
resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==}
engines: {node: '>=10'}
istanbul-reports@3.1.7:
resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==}
engines: {node: '>=8'}
jackspeak@3.4.3: jackspeak@3.4.3:
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
@ -6887,6 +6926,10 @@ packages:
engines: {node: '>=10'} engines: {node: '>=10'}
hasBin: true hasBin: true
test-exclude@7.0.1:
resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==}
engines: {node: '>=18'}
text-encoding-shim@1.0.5: text-encoding-shim@1.0.5:
resolution: {integrity: sha512-H7yYW+jRn4yhu60ygZ2f/eMhXPITRt4QSUTKzLm+eCaDsdX8avmgWpmtmHAzesjBVUTAypz9odu5RKUjX5HNYA==} resolution: {integrity: sha512-H7yYW+jRn4yhu60ygZ2f/eMhXPITRt4QSUTKzLm+eCaDsdX8avmgWpmtmHAzesjBVUTAypz9odu5RKUjX5HNYA==}
@ -7505,6 +7548,7 @@ packages:
workbox-google-analytics@7.0.0: workbox-google-analytics@7.0.0:
resolution: {integrity: sha512-MEYM1JTn/qiC3DbpvP2BVhyIH+dV/5BjHk756u9VbwuAhu0QHyKscTnisQuz21lfRpOwiS9z4XdqeVAKol0bzg==} resolution: {integrity: sha512-MEYM1JTn/qiC3DbpvP2BVhyIH+dV/5BjHk756u9VbwuAhu0QHyKscTnisQuz21lfRpOwiS9z4XdqeVAKol0bzg==}
deprecated: It is not compatible with newer versions of GA starting with v4, as long as you are using GAv3 it should be ok, but the package is not longer being maintained
workbox-navigation-preload@7.0.0: workbox-navigation-preload@7.0.0:
resolution: {integrity: sha512-juWCSrxo/fiMz3RsvDspeSLGmbgC0U9tKqcUPZBCf35s64wlaLXyn2KdHHXVQrb2cqF7I0Hc9siQalainmnXJA==} resolution: {integrity: sha512-juWCSrxo/fiMz3RsvDspeSLGmbgC0U9tKqcUPZBCf35s64wlaLXyn2KdHHXVQrb2cqF7I0Hc9siQalainmnXJA==}
@ -8568,6 +8612,8 @@ snapshots:
'@babel/helper-string-parser': 7.25.9 '@babel/helper-string-parser': 7.25.9
'@babel/helper-validator-identifier': 7.25.9 '@babel/helper-validator-identifier': 7.25.9
'@bcoe/v8-coverage@1.0.2': {}
'@codemirror/autocomplete@6.18.4': '@codemirror/autocomplete@6.18.4':
dependencies: dependencies:
'@codemirror/language': 6.10.8 '@codemirror/language': 6.10.8
@ -8934,6 +8980,8 @@ snapshots:
'@isaacs/string-locale-compare@1.1.0': {} '@isaacs/string-locale-compare@1.1.0': {}
'@istanbuljs/schema@0.1.3': {}
'@jest/schemas@29.6.3': '@jest/schemas@29.6.3':
dependencies: dependencies:
'@sinclair/typebox': 0.27.8 '@sinclair/typebox': 0.27.8
@ -10187,6 +10235,24 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@vitest/coverage-v8@3.0.4(vitest@3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0))':
dependencies:
'@ampproject/remapping': 2.3.0
'@bcoe/v8-coverage': 1.0.2
debug: 4.4.0
istanbul-lib-coverage: 3.2.2
istanbul-lib-report: 3.0.1
istanbul-lib-source-maps: 5.0.6
istanbul-reports: 3.1.7
magic-string: 0.30.17
magicast: 0.3.5
std-env: 3.8.0
test-exclude: 7.0.1
tinyrainbow: 2.0.0
vitest: 3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0)
transitivePeerDependencies:
- supports-color
'@vitest/expect@3.0.4': '@vitest/expect@3.0.4':
dependencies: dependencies:
'@vitest/spy': 3.0.4 '@vitest/spy': 3.0.4
@ -12202,6 +12268,8 @@ snapshots:
dependencies: dependencies:
web-tree-sitter: 0.20.8 web-tree-sitter: 0.20.8
html-escaper@2.0.2: {}
html-escaper@3.0.3: {} html-escaper@3.0.3: {}
html-void-elements@3.0.0: {} html-void-elements@3.0.0: {}
@ -12540,6 +12608,27 @@ snapshots:
isobject@3.0.1: {} isobject@3.0.1: {}
istanbul-lib-coverage@3.2.2: {}
istanbul-lib-report@3.0.1:
dependencies:
istanbul-lib-coverage: 3.2.2
make-dir: 4.0.0
supports-color: 7.2.0
istanbul-lib-source-maps@5.0.6:
dependencies:
'@jridgewell/trace-mapping': 0.3.25
debug: 4.4.0
istanbul-lib-coverage: 3.2.2
transitivePeerDependencies:
- supports-color
istanbul-reports@3.1.7:
dependencies:
html-escaper: 2.0.2
istanbul-lib-report: 3.0.1
jackspeak@3.4.3: jackspeak@3.4.3:
dependencies: dependencies:
'@isaacs/cliui': 8.0.2 '@isaacs/cliui': 8.0.2
@ -15262,6 +15351,12 @@ snapshots:
commander: 2.20.3 commander: 2.20.3
source-map-support: 0.5.21 source-map-support: 0.5.21
test-exclude@7.0.1:
dependencies:
'@istanbuljs/schema': 0.1.3
glob: 10.4.5
minimatch: 9.0.5
text-encoding-shim@1.0.5: {} text-encoding-shim@1.0.5: {}
text-extensions@1.9.0: {} text-extensions@1.9.0: {}

View file

@ -9687,6 +9687,59 @@ exports[`runs examples > example "wchooseCycles" example index 1 1`] = `
] ]
`; `;
exports[`runs examples > example "wchooseCycles" example index 2 1`] = `
[
"[ 0/1 → 1/32 | s:bd ]",
"[ 3/32 → 1/8 | s:bd ]",
"[ 3/16 → 7/32 | s:bd ]",
"[ 1/4 → 1/3 | s:hh ]",
"[ 1/3 → 5/12 | s:hh ]",
"[ 5/12 → 1/2 | s:hh ]",
"[ 1/2 → 7/12 | s:hh ]",
"[ 7/12 → 2/3 | s:hh ]",
"[ 2/3 → 3/4 | s:hh ]",
"[ 3/4 → 5/6 | s:hh ]",
"[ 5/6 → 11/12 | s:hh ]",
"[ 11/12 → 1/1 | s:hh ]",
"[ 1/1 → 33/32 | s:bd ]",
"[ 35/32 → 9/8 | s:bd ]",
"[ 19/16 → 39/32 | s:bd ]",
"[ 5/4 → 4/3 | s:hh ]",
"[ 4/3 → 17/12 | s:hh ]",
"[ 17/12 → 3/2 | s:hh ]",
"[ 3/2 → 49/32 | s:bd ]",
"[ 51/32 → 13/8 | s:bd ]",
"[ 27/16 → 55/32 | s:bd ]",
"[ 7/4 → 11/6 | s:hh ]",
"[ 11/6 → 23/12 | s:hh ]",
"[ 23/12 → 2/1 | s:hh ]",
"[ 2/1 → 25/12 | s:hh ]",
"[ 25/12 → 13/6 | s:hh ]",
"[ 13/6 → 9/4 | s:hh ]",
"[ 9/4 → 7/3 | s:hh ]",
"[ 7/3 → 29/12 | s:hh ]",
"[ 29/12 → 5/2 | s:hh ]",
"[ 5/2 → 81/32 | s:bd ]",
"[ 83/32 → 21/8 | s:bd ]",
"[ 43/16 → 87/32 | s:bd ]",
"[ 11/4 → 17/6 | s:hh ]",
"[ 17/6 → 35/12 | s:hh ]",
"[ 35/12 → 3/1 | s:hh ]",
"[ 3/1 → 97/32 | s:bd ]",
"[ 99/32 → 25/8 | s:bd ]",
"[ 51/16 → 103/32 | s:bd ]",
"[ 13/4 → 10/3 | s:hh ]",
"[ 10/3 → 41/12 | s:hh ]",
"[ 41/12 → 7/2 | s:hh ]",
"[ 7/2 → 43/12 | s:hh ]",
"[ 43/12 → 11/3 | s:hh ]",
"[ 11/3 → 15/4 | s:hh ]",
"[ 15/4 → 23/6 | s:hh ]",
"[ 23/6 → 47/12 | s:hh ]",
"[ 47/12 → 4/1 | s:hh ]",
]
`;
exports[`runs examples > example "when" example index 0 1`] = ` exports[`runs examples > example "when" example index 0 1`] = `
[ [
"[ 0/1 → 1/3 | note:c3 ]", "[ 0/1 → 1/3 | note:c3 ]",

View file

@ -36,8 +36,6 @@ export function Showcase() {
} }
let _videos = [ let _videos = [
{ title: 'Coding Music With Strudel Workhop by Dan Gorelick and Viola He', id: 'oqyAJ4WeKoU' },
{ title: 'Hexe - playing w strudel live coding music', id: '03m3F5xVOMg' },
{ title: 'DJ_Dave - Array [Lil Data Edit]', id: 'KUujFuTcuKc' }, { title: 'DJ_Dave - Array [Lil Data Edit]', id: 'KUujFuTcuKc' },
{ title: 'DJ_Dave - Bitrot [v10101a Edit]', id: 'z_cJMdBp67Q' }, { title: 'DJ_Dave - Bitrot [v10101a Edit]', id: 'z_cJMdBp67Q' },
{ title: 'you will not steve reich your way out of it', id: 'xpILnXcWyuo' }, { title: 'you will not steve reich your way out of it', id: 'xpILnXcWyuo' },
@ -58,7 +56,6 @@ let _videos = [
}, },
{ title: 'letSeaTstrudeL @ solstice stream 2023', id: 'fTiX6dVtdWQ' }, { title: 'letSeaTstrudeL @ solstice stream 2023', id: 'fTiX6dVtdWQ' },
{ title: 'totalgee (Glen F) @ solstice stream 2023', id: 'IvI6uaE3nLU' }, { title: 'totalgee (Glen F) @ solstice stream 2023', id: 'IvI6uaE3nLU' },
{ title: 'Dan Gorelick @ solstice stream 2023', id: 'qMJEljJyPi0' },
// //
/* { // not sure if this is copyrighted ... /* { // not sure if this is copyrighted ...
title: 'Creative Coding @ Chalmers University of Technology, video by svt.se', title: 'Creative Coding @ Chalmers University of Technology, video by svt.se',
@ -126,6 +123,11 @@ let _videos = [
'A first foray into combining (an early version) strudel and hydra, using flok for collaborative coding.', 'A first foray into combining (an early version) strudel and hydra, using flok for collaborative coding.',
}, },
{ title: 'froos @ Algorave 10th Birthday stream', id: 'IcMSocdKwvw' }, { title: 'froos @ Algorave 10th Birthday stream', id: 'IcMSocdKwvw' },
{ title: 'todepasta 1.5', id: 'gCwaVu1Mijg' },
{ title: 'Djenerative Music by Bogdan Vera @ TOPLAP solstice Dec 2024', id: 'LtMX4Lr1nzY' },
{ title: 'La musique by BuboBubo @ TOPLAP solstice Dec 2024', id: 'Oz00Y_f80wU' },
{ title: 'Livecode and vocal breaks by Switch Angel @ TOPLAP solstice Dec 2024', id: '2kzjOIsL6CM' },
{ title: 'Eddyflux algorave set @ rudolf5', id: 'MXz8131Ut0A' },
]; ];
_shuffled = shuffleArray(_videos); _shuffled = shuffleArray(_videos);

View file

@ -0,0 +1,67 @@
import { Textbox } from '../textbox/Textbox';
import cx from '@src/cx.mjs';
function IncButton({ children, className, ...buttonProps }) {
return (
<button
tabIndex={-1}
className={cx(
'border border-transparent p-1 text-center text-sm transition-all hover:bg-foreground active:bg-lineBackground disabled:pointer-events-none disabled:opacity-50 disabled:shadow-none',
className,
)}
type="button"
{...buttonProps}
>
{children}
</button>
);
}
export function Incrementor({
onChange,
value,
min = -Infinity,
max = Infinity,
className,
incrementLabel = 'next page',
decrementLabel = 'prev page',
...incrementorProps
}) {
value = parseInt(value);
value = isNaN(value) ? '' : value;
return (
<div className={cx('w-fit bg-background relative flex items-center"> rounded-md', className)}>
<Textbox
min={min}
max={max}
onChange={(v) => {
if (v.length && v < min) {
return;
}
onChange(v);
}}
type="number"
placeholder=""
value={value}
className="w-32 mb-0 mt-0 border-none rounded-r-none bg-transparent appearance-none [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
{...incrementorProps}
/>
<div className="flex gap-1 ">
<IncButton disabled={value <= min} onClick={() => onChange(value - 1)} aria-label={decrementLabel}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" className="w-4 h-4">
<path d="M3.75 7.25a.75.75 0 0 0 0 1.5h8.5a.75.75 0 0 0 0-1.5h-8.5Z" />
</svg>
</IncButton>
<IncButton
className="rounded-r-md"
disabled={value >= max}
onClick={() => onChange(value + 1)}
aria-label={incrementLabel}
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" className="w-4 h-4">
<path d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z" />
</svg>
</IncButton>
</div>
</div>
);
}

View file

@ -0,0 +1,5 @@
import { Incrementor } from '../incrementor/Incrementor';
export function Pagination({ currPage, onPageChange, className, ...incrementorProps }) {
return <Incrementor min={1} value={currPage} onChange={onPageChange} className={className} {...incrementorProps} />;
}

View file

@ -1,6 +1,8 @@
import { import {
exportPatterns, exportPatterns,
importPatterns, importPatterns,
loadAndSetFeaturedPatterns,
loadAndSetPublicPatterns,
patternFilterName, patternFilterName,
useActivePattern, useActivePattern,
useViewingPatternData, useViewingPatternData,
@ -12,10 +14,10 @@ import { useExamplePatterns } from '../../useExamplePatterns.jsx';
import { parseJSON, isUdels } from '../../util.mjs'; import { parseJSON, isUdels } from '../../util.mjs';
import { ButtonGroup } from './Forms.jsx'; import { ButtonGroup } from './Forms.jsx';
import { settingsMap, useSettings } from '../../../settings.mjs'; import { settingsMap, useSettings } from '../../../settings.mjs';
import { Pagination } from '../pagination/Pagination.jsx';
function classNames(...classes) { import { useState } from 'react';
return classes.filter(Boolean).join(' '); import { useDebounce } from '../usedebounce.jsx';
} import cx from '@src/cx.mjs';
export function PatternLabel({ pattern } /* : { pattern: Tables<'code'> } */) { export function PatternLabel({ pattern } /* : { pattern: Tables<'code'> } */) {
const meta = useMemo(() => getMetadata(pattern.code), [pattern]); const meta = useMemo(() => getMetadata(pattern.code), [pattern]);
@ -33,13 +35,15 @@ export function PatternLabel({ pattern } /* : { pattern: Tables<'code'> } */) {
if (title == null) { if (title == null) {
title = 'unnamed'; title = 'unnamed';
} }
return <>{`${pattern.id}: ${title} by ${Array.isArray(meta.by) ? meta.by.join(',') : 'Anonymous'}`}</>;
const author = Array.isArray(meta.by) ? meta.by.join(',') : 'Anonymous';
return <>{`${pattern.id}: ${title} by ${author.slice(0, 100)}`.slice(0, 60)}</>;
} }
function PatternButton({ showOutline, onClick, pattern, showHiglight }) { function PatternButton({ showOutline, onClick, pattern, showHiglight }) {
return ( return (
<a <a
className={classNames( className={cx(
'mr-4 hover:opacity-50 cursor-pointer block', 'mr-4 hover:opacity-50 cursor-pointer block',
showOutline && 'outline outline-1', showOutline && 'outline outline-1',
showHiglight && 'bg-selection', showHiglight && 'bg-selection',
@ -84,82 +88,72 @@ function ActionButton({ children, onClick, label, labelIsHidden }) {
); );
} }
export function PatternsTab({ context }) { const updateCodeWindow = (context, patternData, reset = false) => {
context.handleUpdate(patternData, reset);
};
const autoResetPatternOnChange = !isUdels();
function UserPatterns({ context }) {
const activePattern = useActivePattern(); const activePattern = useActivePattern();
const viewingPatternStore = useViewingPatternData(); const viewingPatternStore = useViewingPatternData();
const viewingPatternData = parseJSON(viewingPatternStore); const viewingPatternData = parseJSON(viewingPatternStore);
const { userPatterns, patternFilter } = useSettings(); const { userPatterns, patternFilter } = useSettings();
const examplePatterns = useExamplePatterns();
const collections = examplePatterns.collections;
const updateCodeWindow = (patternData, reset = false) => {
context.handleUpdate(patternData, reset);
};
const viewingPatternID = viewingPatternData?.id; const viewingPatternID = viewingPatternData?.id;
const autoResetPatternOnChange = !isUdels();
return ( return (
<div className="px-4 w-full text-foreground space-y-2 flex flex-col overflow-hidden max-h-full h-full"> <div className="flex flex-col gap-2 flex-grow overflow-hidden h-full pb-2 ">
<ButtonGroup <div className="pr-4 space-x-4 flex max-w-full overflow-x-auto">
value={patternFilter} <ActionButton
onChange={(value) => settingsMap.setKey('patternFilter', value)} label="new"
items={patternFilterName} onClick={() => {
></ButtonGroup> const { data } = userPattern.createAndAddToDB();
{patternFilter === patternFilterName.user && ( updateCodeWindow(context, data);
<div> }}
<div className="pr-4 space-x-4 border-b border-foreground flex max-w-full overflow-x-auto"> />
<ActionButton <ActionButton
label="new" label="duplicate"
onClick={() => { onClick={() => {
const { data } = userPattern.createAndAddToDB(); const { data } = userPattern.duplicate(viewingPatternData);
updateCodeWindow(data); updateCodeWindow(context, data);
}} }}
/> />
<ActionButton <ActionButton
label="duplicate" label="delete"
onClick={() => { onClick={() => {
const { data } = userPattern.duplicate(viewingPatternData); const { data } = userPattern.delete(viewingPatternID);
updateCodeWindow(data); updateCodeWindow(context, { ...data, collection: userPattern.collection });
}} }}
/> />
<ActionButton <label className="hover:opacity-50 cursor-pointer">
label="delete" <input
onClick={() => { style={{ display: 'none' }}
const { data } = userPattern.delete(viewingPatternID); type="file"
updateCodeWindow({ ...data, collection: userPattern.collection }); multiple
}} accept="text/plain,application/json"
/> onChange={(e) => importPatterns(e.target.files)}
<label className="hover:opacity-50 cursor-pointer"> />
<input import
style={{ display: 'none' }} </label>
type="file" <ActionButton label="export" onClick={exportPatterns} />
multiple
accept="text/plain,application/json"
onChange={(e) => importPatterns(e.target.files)}
/>
import
</label>
<ActionButton label="export" onClick={exportPatterns} />
<ActionButton <ActionButton
label="delete-all" label="delete-all"
onClick={() => { onClick={() => {
const { data } = userPattern.clearAll(); const { data } = userPattern.clearAll();
updateCodeWindow(data); updateCodeWindow(context, data);
}} }}
/> />
</div> </div>
</div>
)}
<section className="flex overflow-y-auto max-h-full flex-grow flex-col"> <div className="overflow-auto h-full bg-background p-2 rounded-md">
{patternFilter === patternFilterName.user && ( {patternFilter === patternFilterName.user && (
<PatternButtons <PatternButtons
onClick={(id) => onClick={(id) =>
updateCodeWindow({ ...userPatterns[id], collection: userPattern.collection }, autoResetPatternOnChange) updateCodeWindow(
context,
{ ...userPatterns[id], collection: userPattern.collection },
autoResetPatternOnChange,
)
} }
patterns={userPatterns} patterns={userPatterns}
started={context.started} started={context.started}
@ -167,23 +161,111 @@ export function PatternsTab({ context }) {
viewingPatternID={viewingPatternID} viewingPatternID={viewingPatternID}
/> />
)} )}
{patternFilter !== patternFilterName.user && </div>
Array.from(collections.keys()).map((collection) => { </div>
const patterns = collections.get(collection); );
return ( }
<section key={collection} className="py-2">
<h2 className="text-xl mb-2">{collection}</h2> function PatternPageWithPagination({ patterns, patternOnClick, context, paginationOnChange, initialPage }) {
const [page, setPage] = useState(initialPage);
<PatternButtons const debouncedPageChange = useDebounce(() => {
onClick={(id) => updateCodeWindow({ ...patterns[id], collection }, autoResetPatternOnChange)} paginationOnChange(page);
started={context.started} });
patterns={patterns}
activePattern={activePattern} const onPageChange = (pageNum) => {
/> setPage(pageNum);
</section> debouncedPageChange();
); };
})}
</section> const activePattern = useActivePattern();
return (
<div className="flex flex-grow flex-col h-full overflow-hidden justify-between">
<div className="overflow-auto flex flex-col flex-grow bg-background p-2 rounded-md ">
<PatternButtons
onClick={(id) => patternOnClick(id)}
started={context.started}
patterns={patterns}
activePattern={activePattern}
/>
</div>
<div className="flex items-center gap-2 py-2">
<label htmlFor="pattern pagination">Page</label>
<Pagination id="pattern pagination" currPage={page} onPageChange={onPageChange} />
</div>
</div>
);
}
let featuredPageNum = 1;
function FeaturedPatterns({ context }) {
const examplePatterns = useExamplePatterns();
const collections = examplePatterns.collections;
const patterns = collections.get(patternFilterName.featured);
return (
<PatternPageWithPagination
patterns={patterns}
context={context}
initialPage={featuredPageNum}
patternOnClick={(id) => {
updateCodeWindow(
context,
{ ...patterns[id], collection: patternFilterName.featured },
autoResetPatternOnChange,
);
}}
paginationOnChange={async (pageNum) => {
await loadAndSetFeaturedPatterns(pageNum - 1);
featuredPageNum = pageNum;
}}
/>
);
}
let latestPageNum = 1;
function LatestPatterns({ context }) {
const examplePatterns = useExamplePatterns();
const collections = examplePatterns.collections;
const patterns = collections.get(patternFilterName.public);
return (
<PatternPageWithPagination
patterns={patterns}
context={context}
initialPage={latestPageNum}
patternOnClick={(id) => {
updateCodeWindow(context, { ...patterns[id], collection: patternFilterName.public }, autoResetPatternOnChange);
}}
paginationOnChange={async (pageNum) => {
await loadAndSetPublicPatterns(pageNum - 1);
latestPageNum = pageNum;
}}
/>
);
}
function PublicPatterns({ context }) {
const { patternFilter } = useSettings();
if (patternFilter === patternFilterName.featured) {
return <FeaturedPatterns context={context} />;
}
return <LatestPatterns context={context} />;
}
export function PatternsTab({ context }) {
const { patternFilter } = useSettings();
return (
<div className="px-4 w-full text-foreground space-y-2 flex flex-col overflow-hidden max-h-full h-full">
<ButtonGroup
value={patternFilter}
onChange={(value) => settingsMap.setKey('patternFilter', value)}
items={patternFilterName}
></ButtonGroup>
{patternFilter === patternFilterName.user ? (
<UserPatterns context={context} />
) : (
<PublicPatterns context={context} />
)}
</div> </div>
); );
} }

View file

@ -1,7 +1,7 @@
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import jsdocJson from '../../../../../doc.json'; import jsdocJson from '../../../../../doc.json';
import { Textbox } from '../textbox/Textbox';
const availableFunctions = jsdocJson.docs const availableFunctions = jsdocJson.docs
.filter(({ name, description }) => name && !name.startsWith('_') && !!description) .filter(({ name, description }) => name && !name.startsWith('_') && !!description)
.sort((a, b) => /* a.meta.filename.localeCompare(b.meta.filename) + */ a.name.localeCompare(b.name)); .sort((a, b) => /* a.meta.filename.localeCompare(b.meta.filename) + */ a.name.localeCompare(b.name));
@ -29,12 +29,7 @@ export function Reference() {
<div className="flex h-full w-full p-2 overflow-hidden"> <div className="flex h-full w-full p-2 overflow-hidden">
<div className="h-full flex flex-col gap-2 w-1/3 max-w-72 "> <div className="h-full flex flex-col gap-2 w-1/3 max-w-72 ">
<div class="w-full flex"> <div class="w-full flex">
<input <Textbox className="w-full" placeholder="Search" value={search} onChange={setSearch} />
className="w-full p-1 bg-background rounded-md border-none"
placeholder="Search"
value={search}
onInput={(event) => setSearch(event.target.value)}
/>
</div> </div>
<div className="flex flex-col h-full overflow-y-auto gap-1.5 bg-background bg-opacity-50 rounded-md"> <div className="flex flex-col h-full overflow-y-auto gap-1.5 bg-background bg-opacity-50 rounded-md">
{visibleFunctions.map((entry, i) => ( {visibleFunctions.map((entry, i) => (

View file

@ -5,6 +5,7 @@ import { useMemo, useRef, useState } from 'react';
import { settingsMap, useSettings } from '../../../settings.mjs'; import { settingsMap, useSettings } from '../../../settings.mjs';
import { ButtonGroup } from './Forms.jsx'; import { ButtonGroup } from './Forms.jsx';
import ImportSoundsButton from './ImportSoundsButton.jsx'; import ImportSoundsButton from './ImportSoundsButton.jsx';
import { Textbox } from '../textbox/Textbox.jsx';
const getSamples = (samples) => const getSamples = (samples) =>
Array.isArray(samples) ? samples.length : typeof samples === 'object' ? Object.values(samples).length : 1; Array.isArray(samples) ? samples.length : typeof samples === 'object' ? Object.values(samples).length : 1;
@ -52,13 +53,8 @@ export function SoundsTab() {
}); });
return ( return (
<div id="sounds-tab" className="px-4 flex flex-col w-full h-full text-foreground"> <div id="sounds-tab" className="px-4 flex flex-col w-full h-full">
<input <Textbox placeholder="Search" value={search} onChange={(v) => setSearch(v)} />
className="w-full p-1 bg-background rounded-md my-2"
placeholder="Search"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<div className="pb-2 flex shrink-0 flex-wrap"> <div className="pb-2 flex shrink-0 flex-wrap">
<ButtonGroup <ButtonGroup

View file

@ -46,7 +46,8 @@ export function WelcomeTab({ context }) {
<a href="https://github.com/tidalcycles/strudel" target="_blank"> <a href="https://github.com/tidalcycles/strudel" target="_blank">
github github
</a> </a>
. Please consider to{' '} . You can also find <a href="https://github.com/felixroos/dough-samples/blob/main/README.md">licensing info</a>{' '}
for the default sound banks there. Please consider to{' '}
<a href="https://opencollective.com/tidalcycles" target="_blank"> <a href="https://opencollective.com/tidalcycles" target="_blank">
support this project support this project
</a>{' '} </a>{' '}

View file

@ -0,0 +1,11 @@
import cx from '@src/cx.mjs';
export function Textbox({ onChange, className, ...inputProps }) {
return (
<input
className={cx('p-1 bg-background rounded-md my-2 border-foreground', className)}
onChange={(e) => onChange(e.target.value)}
{...inputProps}
/>
);
}

View file

@ -0,0 +1,30 @@
import { useMemo } from 'react';
import { useEffect } from 'react';
import { useRef } from 'react';
function debounce(fn, wait) {
let timer;
return function (...args) {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => fn(...args), wait);
};
}
export function useDebounce(callback) {
const ref = useRef;
useEffect(() => {
ref.current = callback;
}, [callback]);
const debouncedCallback = useMemo(() => {
const func = () => {
ref.current?.();
};
return debounce(func, 1000);
}, []);
return debouncedCallback;
}

View file

@ -1,4 +1,4 @@
import { $featuredPatterns, $publicPatterns, collectionName } from '../user_pattern_utils.mjs'; import { $featuredPatterns, $publicPatterns, patternFilterName } from '../user_pattern_utils.mjs';
import { useStore } from '@nanostores/react'; import { useStore } from '@nanostores/react';
import { useMemo } from 'react'; import { useMemo } from 'react';
import * as tunes from '../repl/tunes.mjs'; import * as tunes from '../repl/tunes.mjs';
@ -12,9 +12,9 @@ export const useExamplePatterns = () => {
const publicPatterns = useStore($publicPatterns); const publicPatterns = useStore($publicPatterns);
const collections = useMemo(() => { const collections = useMemo(() => {
const pats = new Map(); const pats = new Map();
pats.set(collectionName.featured, featuredPatterns); pats.set(patternFilterName.featured, featuredPatterns);
pats.set(collectionName.public, publicPatterns); pats.set(patternFilterName.public, publicPatterns);
// pats.set(collectionName.stock, stockPatterns); // pats.set(patternFilterName.stock, stockPatterns);
return pats; return pats;
}, [featuredPatterns, publicPatterns]); }, [featuredPatterns, publicPatterns]);

View file

@ -8,16 +8,12 @@ import { confirmDialog, parseJSON, supabase } from './repl/util.mjs';
export let $publicPatterns = atom([]); export let $publicPatterns = atom([]);
export let $featuredPatterns = atom([]); export let $featuredPatterns = atom([]);
export const collectionName = { const patternQueryLimit = 20;
user: 'user',
public: 'Last Creations',
stock: 'Stock Examples',
featured: 'Featured',
};
export const patternFilterName = { export const patternFilterName = {
community: 'community', public: 'latest',
featured: 'featured',
user: 'user', user: 'user',
// stock: 'stock examples',
}; };
const sessionAtom = (name, initial = undefined) => { const sessionAtom = (name, initial = undefined) => {
@ -36,7 +32,7 @@ const sessionAtom = (name, initial = undefined) => {
export let $viewingPatternData = sessionAtom('viewingPatternData', { export let $viewingPatternData = sessionAtom('viewingPatternData', {
id: '', id: '',
code: '', code: '',
collection: collectionName.user, collection: patternFilterName.user,
created_at: Date.now(), created_at: Date.now(),
}); });
@ -51,25 +47,50 @@ export const setViewingPatternData = (data) => {
$viewingPatternData.set(JSON.stringify(data)); $viewingPatternData.set(JSON.stringify(data));
}; };
export function loadPublicPatterns() { function parsePageNum(page) {
return supabase.from('code_v1').select().eq('public', true).limit(20).order('id', { ascending: false }); return isNaN(page) ? 0 : page;
}
export function loadPublicPatterns(page) {
page = parsePageNum(page);
const offset = page * patternQueryLimit;
return supabase
.from('code_v1')
.select()
.eq('public', true)
.range(offset, offset + patternQueryLimit)
.order('id', { ascending: false });
} }
export function loadFeaturedPatterns() { export function loadFeaturedPatterns(page = 0) {
return supabase.from('code_v1').select().eq('featured', true).limit(20).order('id', { ascending: false }); page = parsePageNum(page);
const offset = page * patternQueryLimit;
return supabase
.from('code_v1')
.select()
.eq('featured', true)
.range(offset, offset + patternQueryLimit)
.order('id', { ascending: false });
}
export async function loadAndSetPublicPatterns(page) {
const p = await loadPublicPatterns(page);
const data = p?.data;
const pats = {};
data?.forEach((data, key) => (pats[data.id ?? key] = data));
$publicPatterns.set(pats);
}
export async function loadAndSetFeaturedPatterns(page) {
const p = await loadFeaturedPatterns(page);
const data = p?.data;
const pats = {};
data?.forEach((data, key) => (pats[data.id ?? key] = data));
$featuredPatterns.set(pats);
} }
export async function loadDBPatterns() { export async function loadDBPatterns() {
try { try {
const { data: publicPatterns } = await loadPublicPatterns(); await loadAndSetPublicPatterns();
const { data: featuredPatterns } = await loadFeaturedPatterns(); await loadAndSetFeaturedPatterns();
const featured = {};
const pub = {};
publicPatterns?.forEach((data, key) => (pub[data.id ?? key] = data));
featuredPatterns?.forEach((data, key) => (featured[data.id ?? key] = data));
$publicPatterns.set(pub);
$featuredPatterns.set(featured);
} catch (err) { } catch (err) {
console.error('error loading patterns', err); console.error('error loading patterns', err);
} }
@ -92,7 +113,7 @@ export const setLatestCode = (code) => settingsMap.setKey('latestCode', code);
const defaultCode = ''; const defaultCode = '';
export const userPattern = { export const userPattern = {
collection: collectionName.user, collection: patternFilterName.user,
getAll() { getAll() {
const patterns = parseJSON(settingsMap.get().userPatterns); const patterns = parseJSON(settingsMap.get().userPatterns);
return patterns ?? {}; return patterns ?? {};