diff --git a/README.md b/README.md index 200faaae..12ee8503 100644 --- a/README.md +++ b/README.md @@ -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). +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 There are many ways to contribute to this project! See [contribution guide](./CONTRIBUTING.md). diff --git a/package.json b/package.json index 9068b2ca..df50f1f4 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "@eslint/eslintrc": "^3.2.0", "@eslint/js": "^9.19.0", "@tauri-apps/cli": "^2.2.7", + "@vitest/coverage-v8": "3.0.4", "@vitest/ui": "^3.0.4", "acorn": "^8.14.0", "dependency-tree": "^11.0.1", diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 4f0d3576..4e5eb15d 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -1244,6 +1244,16 @@ export function reify(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. * * @return {Pattern} diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index 3ab35f23..5348173d 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -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 { 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 { id, keyAlias, getCurrentKeyboardState } from './util.mjs'; @@ -433,19 +433,30 @@ export const chooseCycles = (...xs) => chooseInWith(rand.segment(1), xs); export const randcat = chooseCycles; const _wchooseWith = function (pat, ...pairs) { + // A list of patterns of values const values = pairs.map((pair) => reify(pair[0])); + + // A list of weight patterns const weights = []; - let accum = 0; + + let total = pure(0); for (const pair of pairs) { - accum += pair[1]; - weights.push(accum); + // 'add' accepts either values or patterns of values here, so no need + // 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 find = r * total; - return values[weights.findIndex((x) => x > find, weights)]; + const findpat = total.mul(r); + 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(); @@ -467,6 +478,9 @@ export const wchoose = (...pairs) => wchooseWith(rand, ...pairs); * wchooseCycles(["bd",10], ["hh",1], ["sd",1]).s().fast(8) * @example * 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(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 494d04f0..2cdd501c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -39,6 +39,9 @@ importers: '@tauri-apps/cli': specifier: ^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': specifier: ^3.0.4 version: 3.0.4(vitest@3.0.4) @@ -1412,6 +1415,10 @@ packages: resolution: {integrity: sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg==} 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': resolution: {integrity: sha512-sFAphGQIqyQZfP2ZBsSHV7xQvo9Py0rV0dW7W3IMRdS+zDuNb2l3no78CvUaWKGfzFjI4FTrLdUSj86IGb2hRA==} @@ -1834,6 +1841,10 @@ packages: '@isaacs/string-locale-compare@1.1.0': 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': resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -2818,6 +2829,15 @@ packages: peerDependencies: 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': resolution: {integrity: sha512-Nm5kJmYw6P2BxhJPkO3eKKhGYKRsnqJqf+r0yOGRKpEP+bSCBDsjXgiu1/5QFrnPMEgzfC38ZEjvCFgaNBC0Eg==} @@ -4487,6 +4507,9 @@ packages: hs2js@0.1.0: resolution: {integrity: sha512-THlUIMX8tZf6gtbz5RUZ8xQUyKJEItsx7bxEBcouFIEWjeo90376WMocj3JEz6qTv5nM+tjo3vNvLf89XruMvg==} + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + html-escaper@3.0.3: resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==} @@ -4837,6 +4860,22 @@ packages: resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} 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: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} @@ -6887,6 +6926,10 @@ packages: engines: {node: '>=10'} hasBin: true + test-exclude@7.0.1: + resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==} + engines: {node: '>=18'} + text-encoding-shim@1.0.5: resolution: {integrity: sha512-H7yYW+jRn4yhu60ygZ2f/eMhXPITRt4QSUTKzLm+eCaDsdX8avmgWpmtmHAzesjBVUTAypz9odu5RKUjX5HNYA==} @@ -7505,6 +7548,7 @@ packages: workbox-google-analytics@7.0.0: 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: resolution: {integrity: sha512-juWCSrxo/fiMz3RsvDspeSLGmbgC0U9tKqcUPZBCf35s64wlaLXyn2KdHHXVQrb2cqF7I0Hc9siQalainmnXJA==} @@ -8568,6 +8612,8 @@ snapshots: '@babel/helper-string-parser': 7.25.9 '@babel/helper-validator-identifier': 7.25.9 + '@bcoe/v8-coverage@1.0.2': {} + '@codemirror/autocomplete@6.18.4': dependencies: '@codemirror/language': 6.10.8 @@ -8934,6 +8980,8 @@ snapshots: '@isaacs/string-locale-compare@1.1.0': {} + '@istanbuljs/schema@0.1.3': {} + '@jest/schemas@29.6.3': dependencies: '@sinclair/typebox': 0.27.8 @@ -10187,6 +10235,24 @@ snapshots: transitivePeerDependencies: - 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': dependencies: '@vitest/spy': 3.0.4 @@ -12202,6 +12268,8 @@ snapshots: dependencies: web-tree-sitter: 0.20.8 + html-escaper@2.0.2: {} + html-escaper@3.0.3: {} html-void-elements@3.0.0: {} @@ -12540,6 +12608,27 @@ snapshots: 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: dependencies: '@isaacs/cliui': 8.0.2 @@ -15262,6 +15351,12 @@ snapshots: commander: 2.20.3 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-extensions@1.9.0: {} diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 64d7cc1f..c100d566 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -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`] = ` [ "[ 0/1 → 1/3 | note:c3 ]", diff --git a/website/src/components/Showcase.jsx b/website/src/components/Showcase.jsx index e4e624a9..a32c944a 100644 --- a/website/src/components/Showcase.jsx +++ b/website/src/components/Showcase.jsx @@ -36,8 +36,6 @@ export function Showcase() { } 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 - Bitrot [v10101a Edit]', id: 'z_cJMdBp67Q' }, { 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: 'totalgee (Glen F) @ solstice stream 2023', id: 'IvI6uaE3nLU' }, - { title: 'Dan Gorelick @ solstice stream 2023', id: 'qMJEljJyPi0' }, // /* { // not sure if this is copyrighted ... 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.', }, { 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); diff --git a/website/src/repl/components/incrementor/Incrementor.jsx b/website/src/repl/components/incrementor/Incrementor.jsx new file mode 100644 index 00000000..5f722fb7 --- /dev/null +++ b/website/src/repl/components/incrementor/Incrementor.jsx @@ -0,0 +1,67 @@ +import { Textbox } from '../textbox/Textbox'; +import cx from '@src/cx.mjs'; + +function IncButton({ children, className, ...buttonProps }) { + return ( + + ); +} +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 ( +