diff --git a/.gitignore b/.gitignore index b7704c618..59d9940e1 100644 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,6 @@ fabric.properties .idea/caches/build_file_checksums.ser # END JetBrains -> BEGIN JetBrains + +samples/* +!samples/README.md diff --git a/package.json b/package.json index 9d3632306..979009130 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "format-check": "prettier --check .", "report-undocumented": "npm run jsdoc-json && node jsdoc/undocumented.mjs > undocumented.json", "check": "npm run format-check && npm run lint && npm run test", + "sampler": "cd samples && node ../packages/sampler/sample-server.mjs", "iclc": "cd paper && pandoc --template=pandoc/iclc.html --citeproc --number-sections iclc2023.md -o iclc2023.html && pandoc --template=pandoc/iclc.latex --citeproc --number-sections iclc2023.md -o iclc2023.pdf" }, "repository": { diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs index 5602a0520..d28bf95d1 100644 --- a/packages/codemirror/codemirror.mjs +++ b/packages/codemirror/codemirror.mjs @@ -27,6 +27,7 @@ import { persistentAtom } from '@nanostores/persistent'; const extensions = { isLineWrappingEnabled: (on) => (on ? EditorView.lineWrapping : []), isBracketMatchingEnabled: (on) => (on ? bracketMatching({ brackets: '()[]{}<>' }) : []), + isBracketClosingEnabled: (on) => (on ? closeBrackets() : []), isLineNumbersDisplayed: (on) => (on ? lineNumbers() : []), theme, isAutoCompletionEnabled, @@ -41,6 +42,7 @@ const compartments = Object.fromEntries(Object.keys(extensions).map((key) => [ke export const defaultSettings = { keybindings: 'codemirror', isBracketMatchingEnabled: false, + isBracketClosingEnabled: true, isLineNumbersDisplayed: true, isActiveLineHighlighted: false, isAutoCompletionEnabled: false, @@ -76,7 +78,6 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo widgetPlugin, // indentOnInput(), // works without. already brought with javascript extension? // bracketMatching(), // does not do anything - closeBrackets(), syntaxHighlighting(defaultHighlightStyle), history(), EditorView.updateListener.of((v) => onChange(v)), @@ -309,6 +310,9 @@ export class StrudelMirror { setLineNumbersDisplayed(enabled) { this.reconfigureExtension('isLineNumbersDisplayed', enabled); } + setBracketClosingEnabled(enabled) { + this.reconfigureExtension('isBracketClosingEnabled', enabled); + } setTheme(theme) { this.reconfigureExtension('theme', theme); } diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 6b8951f3b..daacc946e 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -542,7 +542,7 @@ export class Pattern { * @noAutocomplete */ filterValues(value_test) { - return new Pattern((state) => this.query(state).filter((hap) => value_test(hap.value))); + return new Pattern((state) => this.query(state).filter((hap) => value_test(hap.value))).setTactus(this.tactus); } /** @@ -1147,7 +1147,7 @@ Pattern.prototype.factories = { slowcat, fastcat, cat, - timeCat, + timecat, sequence, seq, polymeter, @@ -1254,14 +1254,14 @@ function _stackWith(func, pats) { export function stackLeft(...pats) { return _stackWith( - (tactus, pats) => pats.map((pat) => (pat.tactus.eq(tactus) ? pat : timeCat(pat, gap(tactus.sub(pat.tactus))))), + (tactus, pats) => pats.map((pat) => (pat.tactus.eq(tactus) ? pat : timecat(pat, gap(tactus.sub(pat.tactus))))), pats, ); } export function stackRight(...pats) { return _stackWith( - (tactus, pats) => pats.map((pat) => (pat.tactus.eq(tactus) ? pat : timeCat(gap(tactus.sub(pat.tactus)), pat))), + (tactus, pats) => pats.map((pat) => (pat.tactus.eq(tactus) ? pat : timecat(gap(tactus.sub(pat.tactus)), pat))), pats, ); } @@ -1274,7 +1274,7 @@ export function stackCentre(...pats) { return pat; } const g = gap(tactus.sub(pat.tactus).div(2)); - return timeCat(g, pat, g); + return timecat(g, pat, g); }), pats, ); @@ -1364,13 +1364,13 @@ export function cat(...pats) { * the pattern's 'tactus', generally inferred by the mininotation. * @return {Pattern} * @example - * timeCat([3,"e3"],[1, "g3"]).note() + * timecat([3,"e3"],[1, "g3"]).note() * // the same as "e3@3 g3".note() * @example - * timeCat("bd sd cp","hh hh").sound() + * timecat("bd sd cp","hh hh").sound() * // the same as "bd sd cp hh hh".sound() */ -export function timeCat(...timepats) { +export function timecat(...timepats) { const findtactus = (x) => (Array.isArray(x) ? x : [x.tactus, x]); timepats = timepats.map(findtactus); if (timepats.length == 1) { @@ -1392,6 +1392,9 @@ export function timeCat(...timepats) { return result; } +/** Deprecated alias for `timecat` */ +export const timeCat = timecat; + /** * Allows to arrange multiple patterns together over multiple cycles. * Takes a variable number of arrays with two elements specifying the number of cycles and the pattern to use. @@ -1406,7 +1409,7 @@ export function timeCat(...timepats) { export function arrange(...sections) { const total = sections.reduce((sum, [cycles]) => sum + cycles, 0); sections = sections.map(([cycles, section]) => [cycles, section.fast(cycles)]); - return timeCat(...sections).slow(total); + return timecat(...sections).slow(total); } export function fastcat(...pats) { @@ -1418,14 +1421,20 @@ export function fastcat(...pats) { return result; } +/** See `fastcat` */ +export function sequence(...pats) { + return fastcat(...pats); +} + /** - * Concatenates patterns beatwise, similar to `timeCat`, but if an argument is a list, the whole pattern will be repeated for each element in the list. + * Concatenates patterns stepwise, according to their 'tactus'. + * Similar to `timecat`, but if an argument is a list, the whole pattern will be repeated for each element in the list. * * @return {Pattern} * @example - * beatCat(["bd cp", "mt"], "bd").sound() + * stepcat(["bd cp", "mt"], "bd").sound() */ -export function beatCat(...groups) { +export function stepcat(...groups) { groups = groups.map((a) => (Array.isArray(a) ? a.map(reify) : [reify(a)])); const cycles = lcm(...groups.map((x) => Fraction(x.length))); @@ -1436,16 +1445,11 @@ export function beatCat(...groups) { } result = result.filter((x) => x.tactus > 0); const tactus = result.reduce((a, b) => a.add(b.tactus), Fraction(0)); - result = timeCat(...result); + result = timecat(...result); result.tactus = tactus; return result; } -/** See `fastcat` */ -export function sequence(...pats) { - return fastcat(...pats); -} - /** Like **cat**, but the items are crammed into one cycle. * @synonyms fastcat, sequence * @example diff --git a/packages/core/test/value.test.mjs b/packages/core/test/value.test.mjs index c54726071..87cba57d0 100644 --- a/packages/core/test/value.test.mjs +++ b/packages/core/test/value.test.mjs @@ -22,8 +22,4 @@ describe('Value', () => { expect(valued(mul).ap(3).ap(3).value).toEqual(9); expect(valued(3).mul(3).value).toEqual(9); }); - it('union bare numbers for numeral props', () => { - expect(n(3).cutoff(500).add(10).firstCycleValues).toEqual([{ n: 13, cutoff: 510 }]); - expect(n(3).cutoff(500).mul(2).firstCycleValues).toEqual([{ n: 6, cutoff: 1000 }]); - }); }); diff --git a/packages/core/value.mjs b/packages/core/value.mjs index f9a963ce7..ef98bc370 100644 --- a/packages/core/value.mjs +++ b/packages/core/value.mjs @@ -5,14 +5,13 @@ This program is free software: you can redistribute it and/or modify it under th */ import { curry } from './util.mjs'; +import { logger } from './logger.mjs'; export function unionWithObj(a, b, func) { - if (typeof b?.value === 'number') { - // https://github.com/tidalcycles/strudel/issues/262 - const numKeys = Object.keys(a).filter((k) => typeof a[k] === 'number'); - const numerals = Object.fromEntries(numKeys.map((k) => [k, b.value])); - b = Object.assign(b, numerals); - delete b.value; + if (b?.value !== undefined && Object.keys(b).length === 1) { + // https://github.com/tidalcycles/strudel/issues/1026 + logger(`[warn]: Can't do arithmetic on control pattern.`); + return a; } const common = Object.keys(a).filter((k) => Object.keys(b).includes(k)); return Object.assign({}, a, b, Object.fromEntries(common.map((k) => [k, func(a[k], b[k])]))); diff --git a/packages/sampler/README.md b/packages/sampler/README.md new file mode 100644 index 000000000..c495c2add --- /dev/null +++ b/packages/sampler/README.md @@ -0,0 +1,22 @@ +# @strudel/sampler + +This package allows you to serve your samples on disk to the strudel REPL. + +```sh +cd ~/your/samples/ +npx @strudel/sampler +``` + +This will run a server on `http://localhost:5432`. +You can now load the samples via: + +```js +samples('http://localhost:5432') +``` + +## Options + +```sh +LOG=1 npx @strudel/sampler # adds logging +PORT=5555 npx @strudel/sampler # changes port +``` diff --git a/packages/sampler/package.json b/packages/sampler/package.json new file mode 100644 index 000000000..c457b315a --- /dev/null +++ b/packages/sampler/package.json @@ -0,0 +1,19 @@ +{ + "name": "@strudel/sampler", + "version": "0.0.8", + "description": "", + "keywords": [ + "tidalcycles", + "strudel", + "pattern", + "livecoding", + "algorave" + ], + "author": "Felix Roos ", + "license": "AGPL-3.0-or-later", + "bin": "./sample-server.mjs", + "type": "module", + "dependencies": { + "cowsay": "^1.6.0" + } +} \ No newline at end of file diff --git a/packages/sampler/sample-server.mjs b/packages/sampler/sample-server.mjs new file mode 100644 index 000000000..b9ae64ed6 --- /dev/null +++ b/packages/sampler/sample-server.mjs @@ -0,0 +1,117 @@ +#!/usr/bin/env node + +import cowsay from 'cowsay'; +import { createReadStream } from 'fs'; +import { readdir } from 'fs/promises'; +import http from 'http'; +import { join } from 'path'; +import os from 'os'; + +// eslint-disable-next-line +const LOG = !!process.env.LOG || false; + +console.log( + cowsay.say({ + text: 'welcome to @strudel/sampler', + e: 'oO', + T: 'U ', + }), +); + +async function getFilesInDirectory(directory) { + let files = []; + const dirents = await readdir(directory, { withFileTypes: true }); + for (const dirent of dirents) { + const fullPath = join(directory, dirent.name); + if (dirent.isDirectory()) { + if (dirent.name.startsWith('.')) { + LOG && console.warn(`ignore hidden folder: ${fullPath}`); + continue; + } + try { + const subFiles = (await getFilesInDirectory(fullPath)).filter((f) => + ['wav', 'mp3', 'ogg'].includes(f.split('.').slice(-1)[0].toLowerCase()), + ); + files = files.concat(subFiles); + LOG && console.log(`${dirent.name} (${subFiles.length})`); + } catch (err) { + LOG && console.warn(`skipped due to error: ${fullPath}`); + } + } else { + files.push(fullPath); + } + } + return files; +} + +async function getBanks(directory) { + // const directory = resolve(__dirname, '.'); + let files = await getFilesInDirectory(directory); + let banks = {}; + files = files.map((url) => { + const [bank] = url.split('/').slice(-2); + banks[bank] = banks[bank] || []; + url = url.replace(directory, ''); + banks[bank].push(url); + return url; + }); + banks._base = `http://localhost:5432`; + return { banks, files }; +} + +// eslint-disable-next-line +const directory = process.cwd(); +const server = http.createServer(async (req, res) => { + res.setHeader('Access-Control-Allow-Origin', '*'); + const { banks, files } = await getBanks(directory); + if (req.url === '/') { + res.setHeader('Content-Type', 'application/json'); + return res.end(JSON.stringify(banks)); + } + let subpath = decodeURIComponent(req.url); + if (!files.includes(subpath)) { + res.statusCode = 404; + res.end('File not found'); + return; + } + const filePath = join(directory, subpath); + const readStream = createReadStream(filePath); + readStream.on('error', (err) => { + res.statusCode = 500; + res.end('Internal server error'); + console.error(err); + }); + readStream.pipe(res); +}); + +// eslint-disable-next-line +const PORT = process.env.PORT || 5432; +const IP_ADDRESS = '0.0.0.0'; +let IP; +const networkInterfaces = os.networkInterfaces(); + +Object.keys(networkInterfaces).forEach((key) => { + networkInterfaces[key].forEach((networkInterface) => { + if (networkInterface.family === 'IPv4' && !networkInterface.internal) { + IP = networkInterface.address; + } + }); +}); + +if (!IP) { + console.error("Unable to determine server's IP address."); + // eslint-disable-next-line + process.exit(1); +} + +server.listen(PORT, IP_ADDRESS, () => { + console.log(`@strudel/sampler is now serving audio files from: + ${directory} + +To use them in the Strudel REPL, run: + samples('http://localhost:${PORT}') + +Or on a machine in the same network: + samples('http://${IP}:${PORT}') +`); +}); diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index 85fccc97b..10087fc61 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -194,6 +194,9 @@ export const samples = async (sampleMap, baseUrl = sampleMap._base || '', option if (sampleMap.startsWith('github:')) { sampleMap = githubPath(sampleMap, 'strudel.json'); } + if (sampleMap.startsWith('local:')) { + sampleMap = `http://localhost:5432`; + } if (sampleMap.startsWith('shabda:')) { let [_, path] = sampleMap.split('shabda:'); sampleMap = `https://shabda.ndre.gr/${path}.json?strudel=1`; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cef19f748..74697ee62 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -361,6 +361,12 @@ importers: specifier: ^5.0.10 version: 5.0.10 + packages/sampler: + dependencies: + cowsay: + specifier: ^1.6.0 + version: 1.6.0 + packages/serial: dependencies: '@strudel/core': @@ -5417,10 +5423,10 @@ packages: type-fest: 0.21.3 dev: true - /ansi-regex@2.1.1: - resolution: {integrity: sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==} - engines: {node: '>=0.10.0'} - dev: true + /ansi-regex@3.0.1: + resolution: {integrity: sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==} + engines: {node: '>=4'} + dev: false /ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} @@ -6032,7 +6038,6 @@ packages: /camelcase@5.3.1: resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} engines: {node: '>=6'} - dev: true /camelcase@7.0.1: resolution: {integrity: sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==} @@ -6199,6 +6204,14 @@ packages: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} dev: false + /cliui@6.0.0: + resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + dev: false + /cliui@7.0.4: resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} dependencies: @@ -6261,11 +6274,6 @@ packages: engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} dev: true - /code-point-at@1.1.0: - resolution: {integrity: sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==} - engines: {node: '>=0.10.0'} - dev: true - /collapse-white-space@2.1.0: resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==} dev: false @@ -6533,6 +6541,17 @@ packages: typescript: 5.3.3 dev: true + /cowsay@1.6.0: + resolution: {integrity: sha512-8C4H1jdrgNusTQr3Yu4SCm+ZKsAlDFbpa0KS0Z3im8ueag+9pGOf3CrioruvmeaW/A5oqg9L0ar6qeftAh03jw==} + engines: {node: '>= 4'} + hasBin: true + dependencies: + get-stdin: 8.0.0 + string-width: 2.1.1 + strip-final-newline: 2.0.0 + yargs: 15.4.1 + dev: false + /crelt@1.0.5: resolution: {integrity: sha512-+BO9wPPi+DWTDcNYhr/W90myha8ptzftZT+LwcmUbbok0rcP/fequmFYCw8NMoH7pkAZQzU78b3kYrlua5a9eA==} dev: false @@ -6651,7 +6670,6 @@ packages: /decamelize@1.2.0: resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} engines: {node: '>=0.10.0'} - dev: true /decode-named-character-reference@1.0.2: resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==} @@ -7877,7 +7895,6 @@ packages: /get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} - dev: true /get-east-asian-width@1.2.0: resolution: {integrity: sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==} @@ -7916,6 +7933,11 @@ packages: engines: {node: '>=8'} dev: true + /get-stdin@8.0.0: + resolution: {integrity: sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==} + engines: {node: '>=10'} + dev: false + /get-stream@6.0.0: resolution: {integrity: sha512-A1B3Bh1UmL0bidM/YX2NsCOTnGJePL9rO/M+Mw3m9f2gUpfokS0hi5Eah0WSUEWZdZhIZtMjkIYS7mDfOqNHbg==} engines: {node: '>=10'} @@ -8779,12 +8801,10 @@ packages: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} - /is-fullwidth-code-point@1.0.0: - resolution: {integrity: sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==} - engines: {node: '>=0.10.0'} - dependencies: - number-is-nan: 1.0.1 - dev: true + /is-fullwidth-code-point@2.0.0: + resolution: {integrity: sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==} + engines: {node: '>=4'} + dev: false /is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} @@ -10893,11 +10913,6 @@ packages: set-blocking: 2.0.0 dev: true - /number-is-nan@1.0.1: - resolution: {integrity: sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==} - engines: {node: '>=0.10.0'} - dev: true - /nx@17.2.8: resolution: {integrity: sha512-rM5zXbuXLEuqQqcjVjClyvHwRJwt+NVImR2A6KFNG40Z60HP6X12wAxxeLHF5kXXTDRU0PFhf/yACibrpbPrAw==} hasBin: true @@ -12187,13 +12202,16 @@ packages: /require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} - dev: true /require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} dev: true + /require-main-filename@2.0.0: + resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + dev: false + /requirejs-config-file@4.0.0: resolution: {integrity: sha512-jnIre8cbWOyvr8a5F2KuqBnY+SDA4NXr/hzEZJG79Mxm2WiFQz2dzhC8ibtPJS7zkmBEl1mxSwp5HhC1W4qpxw==} engines: {node: '>=10.13.0'} @@ -12517,7 +12535,6 @@ packages: /set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} - dev: true /set-function-length@1.1.1: resolution: {integrity: sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==} @@ -12890,14 +12907,13 @@ packages: engines: {node: '>=0.10.0'} dev: true - /string-width@1.0.2: - resolution: {integrity: sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==} - engines: {node: '>=0.10.0'} + /string-width@2.1.1: + resolution: {integrity: sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==} + engines: {node: '>=4'} dependencies: - code-point-at: 1.1.0 - is-fullwidth-code-point: 1.0.0 - strip-ansi: 3.0.1 - dev: true + is-fullwidth-code-point: 2.0.0 + strip-ansi: 4.0.0 + dev: false /string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} @@ -12999,12 +13015,12 @@ packages: is-regexp: 1.0.0 dev: true - /strip-ansi@3.0.1: - resolution: {integrity: sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==} - engines: {node: '>=0.10.0'} + /strip-ansi@4.0.0: + resolution: {integrity: sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==} + engines: {node: '>=4'} dependencies: - ansi-regex: 2.1.1 - dev: true + ansi-regex: 3.0.1 + dev: false /strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} @@ -13039,7 +13055,6 @@ packages: /strip-final-newline@2.0.0: resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} engines: {node: '>=6'} - dev: true /strip-final-newline@3.0.0: resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} @@ -14211,6 +14226,10 @@ packages: is-symbol: 1.0.4 dev: true + /which-module@2.0.1: + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + dev: false + /which-pm-runs@1.1.0: resolution: {integrity: sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==} engines: {node: '>=4'} @@ -14267,7 +14286,7 @@ packages: /wide-align@1.1.5: resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} dependencies: - string-width: 1.0.2 + string-width: 4.2.3 dev: true /widest-line@4.0.1: @@ -14459,6 +14478,15 @@ packages: worker-timers-worker: 7.0.67 dev: false + /wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + dev: false + /wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -14551,6 +14579,10 @@ packages: engines: {node: '>=0.4'} dev: true + /y18n@4.0.3: + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + dev: false + /y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -14567,6 +14599,14 @@ packages: engines: {node: '>= 14'} dev: false + /yargs-parser@18.1.3: + resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} + engines: {node: '>=6'} + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + dev: false + /yargs-parser@20.2.9: resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} engines: {node: '>=10'} @@ -14576,6 +14616,23 @@ packages: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} + /yargs@15.4.1: + resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} + engines: {node: '>=8'} + dependencies: + cliui: 6.0.0 + decamelize: 1.2.0 + find-up: 4.1.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + require-main-filename: 2.0.0 + set-blocking: 2.0.0 + string-width: 4.2.3 + which-module: 2.0.1 + y18n: 4.0.3 + yargs-parser: 18.1.3 + dev: false + /yargs@16.2.0: resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} engines: {node: '>=10'} diff --git a/samples/README.md b/samples/README.md new file mode 100644 index 000000000..77a3e388d --- /dev/null +++ b/samples/README.md @@ -0,0 +1,5 @@ +# samples folder + +1. copy any samples to this folder +2. run `npx @strudel/sampler` from this folder +3. add `samples('local:')` to your code diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index a1b2f4a0a..270745902 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -954,31 +954,6 @@ exports[`runs examples > example "bank" example index 0 1`] = ` ] `; -exports[`runs examples > example "beatCat" example index 0 1`] = ` -[ - "[ 0/1 → 1/5 | s:bd ]", - "[ 1/5 → 2/5 | s:cp ]", - "[ 2/5 → 3/5 | s:bd ]", - "[ 3/5 → 4/5 | s:mt ]", - "[ 4/5 → 1/1 | s:bd ]", - "[ 1/1 → 6/5 | s:bd ]", - "[ 6/5 → 7/5 | s:cp ]", - "[ 7/5 → 8/5 | s:bd ]", - "[ 8/5 → 9/5 | s:mt ]", - "[ 9/5 → 2/1 | s:bd ]", - "[ 2/1 → 11/5 | s:bd ]", - "[ 11/5 → 12/5 | s:cp ]", - "[ 12/5 → 13/5 | s:bd ]", - "[ 13/5 → 14/5 | s:mt ]", - "[ 14/5 → 3/1 | s:bd ]", - "[ 3/1 → 16/5 | s:bd ]", - "[ 16/5 → 17/5 | s:cp ]", - "[ 17/5 → 18/5 | s:bd ]", - "[ 18/5 → 19/5 | s:mt ]", - "[ 19/5 → 4/1 | s:bd ]", -] -`; - exports[`runs examples > example "begin" example index 0 1`] = ` [ "[ 0/1 → 1/2 | s:rave begin:0 ]", @@ -5672,27 +5647,6 @@ exports[`runs examples > example "rev" example index 0 1`] = ` ] `; -exports[`runs examples > example "reweight" example index 0 1`] = ` -[ - "[ 0/1 → 1/4 | s:bd ]", - "[ 1/4 → 1/2 | s:sd ]", - "[ 1/2 → 3/4 | s:cp ]", - "[ 3/4 → 1/1 | s:bd ]", - "[ 1/1 → 5/4 | s:sd ]", - "[ 5/4 → 3/2 | s:cp ]", - "[ 3/2 → 7/4 | s:bd ]", - "[ 7/4 → 2/1 | s:sd ]", - "[ 2/1 → 9/4 | s:cp ]", - "[ 9/4 → 5/2 | s:bd ]", - "[ 5/2 → 11/4 | s:sd ]", - "[ 11/4 → 3/1 | s:cp ]", - "[ 3/1 → 13/4 | s:bd ]", - "[ 13/4 → 7/2 | s:sd ]", - "[ 7/2 → 15/4 | s:cp ]", - "[ 15/4 → 4/1 | s:bd ]", -] -`; - exports[`runs examples > example "ribbon" example index 0 1`] = ` [ "[ 0/1 → 1/2 | note:d ]", @@ -7133,6 +7087,31 @@ exports[`runs examples > example "stack" example index 0 2`] = ` ] `; +exports[`runs examples > example "stepcat" example index 0 1`] = ` +[ + "[ 0/1 → 1/5 | s:bd ]", + "[ 1/5 → 2/5 | s:cp ]", + "[ 2/5 → 3/5 | s:bd ]", + "[ 3/5 → 4/5 | s:mt ]", + "[ 4/5 → 1/1 | s:bd ]", + "[ 1/1 → 6/5 | s:bd ]", + "[ 6/5 → 7/5 | s:cp ]", + "[ 7/5 → 8/5 | s:bd ]", + "[ 8/5 → 9/5 | s:mt ]", + "[ 9/5 → 2/1 | s:bd ]", + "[ 2/1 → 11/5 | s:bd ]", + "[ 11/5 → 12/5 | s:cp ]", + "[ 12/5 → 13/5 | s:bd ]", + "[ 13/5 → 14/5 | s:mt ]", + "[ 14/5 → 3/1 | s:bd ]", + "[ 3/1 → 16/5 | s:bd ]", + "[ 16/5 → 17/5 | s:cp ]", + "[ 17/5 → 18/5 | s:bd ]", + "[ 18/5 → 19/5 | s:mt ]", + "[ 19/5 → 4/1 | s:bd ]", +] +`; + exports[`runs examples > example "striate" example index 0 1`] = ` [ "[ 0/1 → 1/6 | s:numbers n:0 begin:0 end:0.16666666666666666 ]", @@ -7319,7 +7298,7 @@ exports[`runs examples > example "sustain" example index 0 1`] = ` ] `; -exports[`runs examples > example "timeCat" example index 0 1`] = ` +exports[`runs examples > example "timecat" example index 0 1`] = ` [ "[ 0/1 → 3/4 | note:e3 ]", "[ 3/4 → 1/1 | note:g3 ]", @@ -7332,7 +7311,7 @@ exports[`runs examples > example "timeCat" example index 0 1`] = ` ] `; -exports[`runs examples > example "timeCat" example index 1 1`] = ` +exports[`runs examples > example "timecat" example index 1 1`] = ` [ "[ 0/1 → 1/5 | s:bd ]", "[ 1/5 → 2/5 | s:sd ]", diff --git a/website/astro.config.mjs b/website/astro.config.mjs index 6389debbc..774e5234e 100644 --- a/website/astro.config.mjs +++ b/website/astro.config.mjs @@ -69,7 +69,7 @@ export default defineConfig({ registerType: 'autoUpdate', injectRegister: 'auto', workbox: { - globPatterns: ['**/*.{js,css,html,ico,png,svg,json,wav,mp3,ogg}'], + globPatterns: ['**/*.{js,css,html,ico,png,svg,json,wav,mp3,ogg,ttf,woff2,TTF,otf}'], runtimeCaching: [ { urlPattern: ({ url }) => diff --git a/website/src/repl/Repl.jsx b/website/src/repl/Repl.jsx index f4c6df258..9c472eb4f 100644 --- a/website/src/repl/Repl.jsx +++ b/website/src/repl/Repl.jsx @@ -61,12 +61,12 @@ async function getModule(name) { export function Repl({ embedded = false }) { const isEmbedded = embedded || isIframe; - const { panelPosition, isZen } = useSettings(); + const { panelPosition, isZen, isSyncEnabled } = useSettings(); const init = useCallback(() => { const drawTime = [-2, 2]; const drawContext = getDrawContext(); const editor = new StrudelMirror({ - sync: false, + sync: isSyncEnabled, defaultOutput: webaudioOutput, getTime: () => getAudioContext().currentTime, setInterval, diff --git a/website/src/repl/panel/SettingsTab.jsx b/website/src/repl/panel/SettingsTab.jsx index 830c926c4..ae2901890 100644 --- a/website/src/repl/panel/SettingsTab.jsx +++ b/website/src/repl/panel/SettingsTab.jsx @@ -81,6 +81,7 @@ export function SettingsTab({ started }) { const { theme, keybindings, + isBracketClosingEnabled, isBracketMatchingEnabled, isLineNumbersDisplayed, isPatternHighlightingEnabled, @@ -88,6 +89,7 @@ export function SettingsTab({ started }) { isAutoCompletionEnabled, isTooltipEnabled, isFlashEnabled, + isSyncEnabled, isLineWrappingEnabled, fontSize, fontFamily, @@ -147,6 +149,11 @@ export function SettingsTab({ started }) { onChange={(cbEvent) => settingsMap.setKey('isBracketMatchingEnabled', cbEvent.target.checked)} value={isBracketMatchingEnabled} /> + settingsMap.setKey('isBracketClosingEnabled', cbEvent.target.checked)} + value={isBracketClosingEnabled} + /> settingsMap.setKey('isLineNumbersDisplayed', cbEvent.target.checked)} @@ -182,6 +189,16 @@ export function SettingsTab({ started }) { onChange={(cbEvent) => settingsMap.setKey('isFlashEnabled', cbEvent.target.checked)} value={isFlashEnabled} /> + { + if (confirm('Changing this setting requires the window to reload itself. OK?')) { + settingsMap.setKey('isSyncEnabled', cbEvent.target.checked); + window.location.reload(); + } + }} + value={isSyncEnabled} + /> Try clicking the logo in the top left! diff --git a/website/src/repl/tunes.mjs b/website/src/repl/tunes.mjs index a89bdf6b1..cffe0c0eb 100644 --- a/website/src/repl/tunes.mjs +++ b/website/src/repl/tunes.mjs @@ -361,8 +361,8 @@ export const echoPiano = `// "Echo piano" // @by Felix Roos n("<0 2 [4 6](3,4,2) 3*2>").color('salmon') -.off(1/4, x=>x.add(2).color('green')) -.off(1/2, x=>x.add(6).color('steelblue')) +.off(1/4, x=>x.add(n(2)).color('green')) +.off(1/2, x=>x.add(n(6)).color('steelblue')) .scale('D minor') .echo(4, 1/8, .5) .clip(.5) diff --git a/website/src/settings.mjs b/website/src/settings.mjs index 34aca7017..a70c42021 100644 --- a/website/src/settings.mjs +++ b/website/src/settings.mjs @@ -8,11 +8,13 @@ export const defaultSettings = { activeFooter: 'intro', keybindings: 'codemirror', isBracketMatchingEnabled: true, + isBracketClosingEnabled: true, isLineNumbersDisplayed: true, isActiveLineHighlighted: true, isAutoCompletionEnabled: false, isTooltipEnabled: false, isFlashEnabled: true, + isSyncEnabled: false, isLineWrappingEnabled: false, isPatternHighlightingEnabled: true, theme: 'strudelTheme', @@ -29,6 +31,8 @@ export const defaultSettings = { export const settingsMap = persistentMap('strudel-settings', defaultSettings); +const parseBoolean = (booleanlike) => ([true, 'true'].includes(booleanlike) ? true : false); + export function useSettings() { const state = useStore(settingsMap); @@ -40,15 +44,17 @@ export function useSettings() { }); return { ...state, - isZen: [true, 'true'].includes(state.isZen) ? true : false, - isBracketMatchingEnabled: [true, 'true'].includes(state.isBracketMatchingEnabled) ? true : false, - isLineNumbersDisplayed: [true, 'true'].includes(state.isLineNumbersDisplayed) ? true : false, - isActiveLineHighlighted: [true, 'true'].includes(state.isActiveLineHighlighted) ? true : false, - isAutoCompletionEnabled: [true, 'true'].includes(state.isAutoCompletionEnabled) ? true : false, - isPatternHighlightingEnabled: [true, 'true'].includes(state.isPatternHighlightingEnabled) ? true : false, - isTooltipEnabled: [true, 'true'].includes(state.isTooltipEnabled) ? true : false, - isLineWrappingEnabled: [true, 'true'].includes(state.isLineWrappingEnabled) ? true : false, - isFlashEnabled: [true, 'true'].includes(state.isFlashEnabled) ? true : false, + isZen: parseBoolean(state.isZen), + isBracketMatchingEnabled: parseBoolean(state.isBracketMatchingEnabled), + isBracketClosingEnabled: parseBoolean(state.isBracketClosingEnabled), + isLineNumbersDisplayed: parseBoolean(state.isLineNumbersDisplayed), + isActiveLineHighlighted: parseBoolean(state.isActiveLineHighlighted), + isAutoCompletionEnabled: parseBoolean(state.isAutoCompletionEnabled), + isPatternHighlightingEnabled: parseBoolean(state.isPatternHighlightingEnabled), + isTooltipEnabled: parseBoolean(state.isTooltipEnabled), + isLineWrappingEnabled: parseBoolean(state.isLineWrappingEnabled), + isFlashEnabled: parseBoolean(state.isFlashEnabled), + isSyncEnabled: parseBoolean(state.isSyncEnabled), fontSize: Number(state.fontSize), panelPosition: state.activeFooter !== '' ? state.panelPosition : 'bottom', // <-- keep this 'bottom' where it is! userPatterns: userPatterns,