From a5b602250614afce28e71d0f5ea93fe258f554a7 Mon Sep 17 00:00:00 2001 From: Kaspars Date: Tue, 7 May 2024 23:56:39 +0200 Subject: [PATCH 001/174] - fix scale() to allow both n() and note() at the same time - fix scale() to allow scale names without tonic and then default it to C --- packages/tonal/test/tonal.test.mjs | 15 +++++++++++++++ packages/tonal/tonal.mjs | 7 ++----- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/packages/tonal/test/tonal.test.mjs b/packages/tonal/test/tonal.test.mjs index 8dd0e1861..968534c80 100644 --- a/packages/tonal/test/tonal.test.mjs +++ b/packages/tonal/test/tonal.test.mjs @@ -30,6 +30,14 @@ describe('tonal', () => { .firstCycleValues.map((h) => h.note), ).toEqual(['C3', 'D3', 'E3']); }); + it('scale with n and note values', () => { + expect( + n(0, 1, 2) + .note(3, 4, 5) + .scale('C major') + .firstCycleValues.map((h) => h.note), + ).toEqual(['F3', 'G3', 'A3']); + }); it('scale with colon', () => { expect( n(0, 1, 2) @@ -37,6 +45,13 @@ describe('tonal', () => { .firstCycleValues.map((h) => h.note), ).toEqual(['C3', 'D3', 'E3']); }); + it('scale without tonic', () => { + expect( + n(0, 1, 2) + .scale('major') + .firstCycleValues.map((h) => h.note), + ).toEqual(['C3', 'D3', 'E3']); + }); it('scale with mininotation colon', () => { expect( n(0, 1, 2) diff --git a/packages/tonal/tonal.mjs b/packages/tonal/tonal.mjs index 4c25d23ea..012a0fc20 100644 --- a/packages/tonal/tonal.mjs +++ b/packages/tonal/tonal.mjs @@ -14,7 +14,7 @@ function scaleStep(step, scale) { scale = scale.replaceAll(':', ' '); step = Math.ceil(step); let { intervals, tonic, empty } = Scale.get(scale); - if ((empty && isNote(scale)) || (!empty && !tonic)) { + if ((empty && isNote(scale)) || (empty && !tonic)) { throw new Error(`incomplete scale. Make sure to use ":" instead of spaces, example: .scale("C:major")`); } else if (empty) { throw new Error(`invalid scale "${scale}"`); @@ -199,10 +199,7 @@ export const scale = register( pat .fmap((value) => { const isObject = typeof value === 'object'; - let step = isObject ? value.n : value; - if (isObject) { - delete value.n; // remove n so it won't cause trouble - } + let step = isObject ? (value.note ?? value.n) : value; if (isNote(step)) { // legacy.. return pure(step); From 4e6d39666a7474f44e28c8e56b9c4f719f88b198 Mon Sep 17 00:00:00 2001 From: Kaspars Date: Wed, 8 May 2024 00:00:58 +0200 Subject: [PATCH 002/174] codeformat --- packages/tonal/test/tonal.test.mjs | 14 +++++++------- packages/tonal/tonal.mjs | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/tonal/test/tonal.test.mjs b/packages/tonal/test/tonal.test.mjs index 968534c80..647fed807 100644 --- a/packages/tonal/test/tonal.test.mjs +++ b/packages/tonal/test/tonal.test.mjs @@ -32,10 +32,10 @@ describe('tonal', () => { }); it('scale with n and note values', () => { expect( - n(0, 1, 2) - .note(3, 4, 5) - .scale('C major') - .firstCycleValues.map((h) => h.note), + n(0, 1, 2) + .note(3, 4, 5) + .scale('C major') + .firstCycleValues.map((h) => h.note), ).toEqual(['F3', 'G3', 'A3']); }); it('scale with colon', () => { @@ -47,9 +47,9 @@ describe('tonal', () => { }); it('scale without tonic', () => { expect( - n(0, 1, 2) - .scale('major') - .firstCycleValues.map((h) => h.note), + n(0, 1, 2) + .scale('major') + .firstCycleValues.map((h) => h.note), ).toEqual(['C3', 'D3', 'E3']); }); it('scale with mininotation colon', () => { diff --git a/packages/tonal/tonal.mjs b/packages/tonal/tonal.mjs index 012a0fc20..29691d192 100644 --- a/packages/tonal/tonal.mjs +++ b/packages/tonal/tonal.mjs @@ -199,7 +199,7 @@ export const scale = register( pat .fmap((value) => { const isObject = typeof value === 'object'; - let step = isObject ? (value.note ?? value.n) : value; + let step = isObject ? value.note ?? value.n : value; if (isNote(step)) { // legacy.. return pure(step); From 33a73c37bc5caf29c7e1f04dbe26501b5727d5f9 Mon Sep 17 00:00:00 2001 From: Kaspars Date: Wed, 8 May 2024 00:46:27 +0200 Subject: [PATCH 003/174] fix tests --- packages/tonal/tonal.mjs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/tonal/tonal.mjs b/packages/tonal/tonal.mjs index 29691d192..acf35a596 100644 --- a/packages/tonal/tonal.mjs +++ b/packages/tonal/tonal.mjs @@ -199,7 +199,15 @@ export const scale = register( pat .fmap((value) => { const isObject = typeof value === 'object'; - let step = isObject ? value.note ?? value.n : value; + let step = value; + if (isObject) { + if (value.note) { + step = value.note; + } else { + step = value.n; + delete value.n; // remove n so it won't cause trouble + } + } if (isNote(step)) { // legacy.. return pure(step); From a4f10d533947f0e6b735e3cfaa40e87439d7e4ee Mon Sep 17 00:00:00 2001 From: Kaspars Date: Wed, 8 May 2024 10:38:06 +0200 Subject: [PATCH 004/174] bug fix --- packages/tonal/test/tonal.test.mjs | 6 +++--- packages/tonal/tonal.mjs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/tonal/test/tonal.test.mjs b/packages/tonal/test/tonal.test.mjs index 647fed807..73c4de151 100644 --- a/packages/tonal/test/tonal.test.mjs +++ b/packages/tonal/test/tonal.test.mjs @@ -33,10 +33,10 @@ describe('tonal', () => { it('scale with n and note values', () => { expect( n(0, 1, 2) - .note(3, 4, 5) + .note(3, 4, 0) .scale('C major') - .firstCycleValues.map((h) => h.note), - ).toEqual(['F3', 'G3', 'A3']); + .firstCycleValues.map((h) => [h.n, h.note]), + ).toEqual([[0, 'F3'], [1, 'G3'], [2, 'C3']]); }); it('scale with colon', () => { expect( diff --git a/packages/tonal/tonal.mjs b/packages/tonal/tonal.mjs index acf35a596..dd723c2da 100644 --- a/packages/tonal/tonal.mjs +++ b/packages/tonal/tonal.mjs @@ -201,7 +201,7 @@ export const scale = register( const isObject = typeof value === 'object'; let step = value; if (isObject) { - if (value.note) { + if (typeof value.note !== 'undefined') { step = value.note; } else { step = value.n; From 8d8b8ab99ed1a18c7b43c6368c1aefcb65988f0e Mon Sep 17 00:00:00 2001 From: Kaspars Date: Wed, 8 May 2024 10:42:06 +0200 Subject: [PATCH 005/174] codeformat --- packages/tonal/test/tonal.test.mjs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/tonal/test/tonal.test.mjs b/packages/tonal/test/tonal.test.mjs index 73c4de151..755b16324 100644 --- a/packages/tonal/test/tonal.test.mjs +++ b/packages/tonal/test/tonal.test.mjs @@ -36,7 +36,11 @@ describe('tonal', () => { .note(3, 4, 0) .scale('C major') .firstCycleValues.map((h) => [h.n, h.note]), - ).toEqual([[0, 'F3'], [1, 'G3'], [2, 'C3']]); + ).toEqual([ + [0, 'F3'], + [1, 'G3'], + [2, 'C3'], + ]); }); it('scale with colon', () => { expect( From 62705da3deef4f9ceb7aaaa031ccc13d7bb3ad20 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 15 Mar 2025 10:29:23 +0100 Subject: [PATCH 006/174] begin uzu package --- packages/uzu/README.md | 29 ++++ packages/uzu/package.json | 37 +++++ packages/uzu/test/uzu.test.mjs | 72 ++++++++ packages/uzu/uzu.mjs | 294 +++++++++++++++++++++++++++++++++ packages/uzu/vite.config.js | 19 +++ pnpm-lock.yaml | 14 +- 6 files changed, 462 insertions(+), 3 deletions(-) create mode 100644 packages/uzu/README.md create mode 100644 packages/uzu/package.json create mode 100644 packages/uzu/test/uzu.test.mjs create mode 100644 packages/uzu/uzu.mjs create mode 100644 packages/uzu/vite.config.js diff --git a/packages/uzu/README.md b/packages/uzu/README.md new file mode 100644 index 000000000..53eef51f8 --- /dev/null +++ b/packages/uzu/README.md @@ -0,0 +1,29 @@ +# uzu + +an experimental parser for an *uzulang*, a custom dsl for patterns that can stand on its own feet. more info: + +- [uzulang I](https://garten.salat.dev/uzu/uzulang1.html) +- [uzulang II](https://garten.salat.dev/uzu/uzulang2.html) + +```js +import { UzuRunner } from 'uzu' + +const runner = UzuRunner({ seq, cat, s, crush, speed, '*': fast }); +const pat = runner.run('s [bd hh*2 cp.(crush 4) ] . speed .8') +``` + +the above code will create the following call structure: + +```lisp +(speed + (s + (seq bd + (* hh 2) + (crush cp 4) + (cat mt ht lt) + ) + ) .8 +) +``` + +you can pass all available functions to *UzuRunner* as an object. diff --git a/packages/uzu/package.json b/packages/uzu/package.json new file mode 100644 index 000000000..9b59078d5 --- /dev/null +++ b/packages/uzu/package.json @@ -0,0 +1,37 @@ +{ + "name": "uzu", + "version": "1.1.0", + "description": "an uzu notation", + "main": "uzu.mjs", + "type": "module", + "publishConfig": { + "main": "dist/uzu.mjs" + }, + "scripts": { + "test": "vitest run", + "bench": "vitest bench", + "build": "vite build", + "prepublishOnly": "npm run build" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/tidalcycles/strudel.git" + }, + "keywords": [ + "tidalcycles", + "strudel", + "pattern", + "livecoding", + "algorave" + ], + "author": "Felix Roos ", + "license": "AGPL-3.0-or-later", + "bugs": { + "url": "https://github.com/tidalcycles/strudel/issues" + }, + "homepage": "https://github.com/tidalcycles/strudel/blob/main/packages/uzu/README.md", + "devDependencies": { + "vite": "^6.0.11", + "vitest": "^3.0.4" + } +} diff --git a/packages/uzu/test/uzu.test.mjs b/packages/uzu/test/uzu.test.mjs new file mode 100644 index 000000000..c58eb1a86 --- /dev/null +++ b/packages/uzu/test/uzu.test.mjs @@ -0,0 +1,72 @@ +/* +uzu.test.mjs - +Copyright (C) 2022 Strudel contributors - see +This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . +*/ + +import { describe, expect, it } from 'vitest'; +import { UzuParser, UzuRunner, printAst } from '../uzu.mjs'; + +const parser = new UzuParser(); +const p = (code) => parser.parse(code); + +describe('uzu s-expressions parser', () => { + it('should parse an empty string', () => expect(p('')).toEqual({ type: 'list', children: [] })); + it('should parse a single item', () => + expect(p('a')).toEqual({ type: 'list', children: [{ type: 'plain', value: 'a' }] })); + it('should parse an empty list', () => expect(p('()')).toEqual({ type: 'list', children: [] })); + it('should parse a list with 1 item', () => + expect(p('(a)')).toEqual({ type: 'list', children: [{ type: 'plain', value: 'a' }] })); + it('should parse a list with 2 items', () => + expect(p('(a b)')).toEqual({ + type: 'list', + children: [ + { type: 'plain', value: 'a' }, + { type: 'plain', value: 'b' }, + ], + })); + it('should parse a list with 2 items', () => + expect(p('(a (b c))')).toEqual({ + type: 'list', + children: [ + { type: 'plain', value: 'a' }, + { + type: 'list', + children: [ + { type: 'plain', value: 'b' }, + { type: 'plain', value: 'c' }, + ], + }, + ], + })); + it('should parse numbers', () => + expect(p('(1 .2 1.2 10 22.3)')).toEqual({ + type: 'list', + children: [ + { type: 'number', value: '1' }, + { type: 'number', value: '.2' }, + { type: 'number', value: '1.2' }, + { type: 'number', value: '10' }, + { type: 'number', value: '22.3' }, + ], + })); +}); + +let desguar = (a) => { + return printAst(parser.parse(a), true); +}; + +describe('uzu sugar', () => { + it('should desugar []', () => expect(desguar('[a b c]')).toEqual('(seq a b c)')); + it('should desugar [] nested', () => expect(desguar('[a [b c]]')).toEqual('(seq a (seq b c))')); + it('should desugar <>', () => expect(desguar('')).toEqual('(cat a b c)')); + it('should desugar <> nested', () => expect(desguar('>')).toEqual('(cat a (cat b c))')); + it('should desugar mixed [] <>', () => expect(desguar('[a ]')).toEqual('(seq a (cat b c))')); + it('should desugar mixed <> []', () => expect(desguar('')).toEqual('(cat a (seq b c))')); + it('should desugar .', () => expect(desguar('s jazz . fast 2')).toEqual('(fast (s jazz) 2)')); + it('should desugar . twice', () => expect(desguar('s jazz . fast 2 . slow 2')).toEqual('(slow (fast (s jazz) 2) 2)')); + it('should desugar README example', () => + expect(desguar('s [bd hh*2 cp.(crush 4) ] . speed .8')).toEqual( + '(speed (s (seq bd (* hh 2) (crush cp 4) (cat mt ht lt))) .8)', + )); +}); diff --git a/packages/uzu/uzu.mjs b/packages/uzu/uzu.mjs new file mode 100644 index 000000000..ead760be0 --- /dev/null +++ b/packages/uzu/uzu.mjs @@ -0,0 +1,294 @@ +/* +uzu.mjs - +Copyright (C) 2022 Strudel contributors - see +This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . +*/ + +// evolved from https://garten.salat.dev/lisp/parser.html +export class UzuParser { + // these are the tokens we expect + token_types = { + string: /^\"(.*?)\"/, + open_list: /^\(/, + close_list: /^\)/, + open_cat: /^\/, + open_seq: /^\[/, + close_seq: /^\]/, + number: /^[0-9]*\.?[0-9]+/, // before pipe! + pipe: /^\./, + stack: /^\,/, + op: /^[\*\/]/, + plain: /^[a-zA-Z0-9\-]+/, + }; + // matches next token + next_token(code) { + for (let type in this.token_types) { + const match = code.match(this.token_types[type]); + if (match) { + return { type, value: match[0] }; + } + } + throw new Error(`zilp: could not match '${code}'`); + } + // takes code string, returns list of matched tokens (if valid) + tokenize(code) { + let tokens = []; + while (code.length > 0) { + code = code.trim(); + const token = this.next_token(code); + code = code.slice(token.value.length); + tokens.push(token); + } + return tokens; + } + // take code, return abstract syntax tree + parse(code) { + this.tokens = this.tokenize(code); + const expressions = []; + while (this.tokens.length) { + expressions.push(this.parse_expr()); + } + if (expressions.length === 0) { + // empty case + return { type: 'list', children: [] }; + } + // do we have multiple top level expressions or a single non list? + if (expressions.length > 1 || expressions[0].type !== 'list') { + return { + type: 'list', + children: this.desugar_children(expressions), + }; + } + // we have a single list + return expressions[0]; + } + // parses any valid expression + parse_expr() { + if (!this.tokens[0]) { + throw new Error(`unexpected end of file`); + } + let next = this.tokens[0]?.type; + if (next === 'open_list') { + return this.parse_list(); + } + if (next === 'open_cat') { + return this.parse_cat(); + } + if (next === 'open_seq') { + return this.parse_seq(); + } + return this.consume(next); + } + desugar_children(children) { + children = this.resolve_ops(children); + children = this.resolve_pipes(children); + return children; + } + // Token[] => Token[][] . returns empty list if type not found + split_children(children, type) { + const chunks = []; + while (true) { + let commaIndex = children.findIndex((child) => child.type === type); + if (commaIndex === -1) break; + const chunk = children.slice(0, commaIndex); + chunks.push(chunk); + children = children.slice(commaIndex + 1); + } + if (!chunks.length) { + return []; + } + chunks.push(children); + return chunks; + } + desugar_stack(children) { + let [type, ...rest] = children; + // children is expected to contain seq or cat as first item + const chunks = this.split_children(rest, 'stack'); + if (!chunks.length) { + // no stack + return children; + } + // collect args of stack function + const args = chunks.map((chunk) => { + if (chunk.length === 1) { + // chunks of one element can be added to the stack as is + return chunk[0]; + } else { + // chunks of multiple args are added to a subsequence of type + return { type: 'list', children: [type, ...chunk] }; + } + }); + return [{ type: 'plain', value: 'stack' }, ...args]; + } + resolve_ops(children) { + while (true) { + let opIndex = children.findIndex((child) => child.type === 'op'); + if (opIndex === -1) break; + const op = { type: 'plain', value: children[opIndex].value }; + if (opIndex === children.length - 1) { + throw new Error(`cannot use operator as last child.`); + } + if (opIndex === 0) { + // regular function call (assuming each operator exists as function) + children[opIndex] = op; + continue; + } + const left = children[opIndex - 1]; + const right = children[opIndex + 1]; + if (left.type === 'pipe') { + // "x !* 2" => (* 2 x) + children[opIndex] = op; + continue; + } + // convert infix to prefix notation + const call = { type: 'list', children: [op, left, right] }; + // insert call while keeping other siblings + children = [...children.slice(0, opIndex - 1), call, ...children.slice(opIndex + 2)]; + // unwrap double list.. e.g. (s jazz) * 2 + if (children.length === 1) { + // there might be a cleaner solution + children = children[0].children; + } + } + return children; + } + resolve_pipes(children) { + while (true) { + let pipeIndex = children.findIndex((child) => child.type === 'pipe'); + // no pipe => we're done + if (pipeIndex === -1) break; + // pipe up front => lambda + if (pipeIndex === 0) { + // . as lambda: (.fast 2) = x=>x.fast(2) + // TODO: this doesn't work for (.fast 2 .speed 2) + // probably needs proper ast representation of lambda + children[pipeIndex] = { type: 'plain', value: '.' }; + continue; + } + const rightSide = children.slice(pipeIndex + 2); + const right = children[pipeIndex + 1]; + if (right.type === 'list') { + // apply function only to left sibling (high precedence) + // s jazz.(fast 2) => s (fast jazz 2) + const [callee, ...rest] = right.children; + const leftSide = children.slice(0, pipeIndex - 1); + const left = children[pipeIndex - 1]; + const args = [callee, left, ...rest]; + const call = { type: 'list', children: args }; + children = [...leftSide, call, ...rightSide]; + } else { + // apply function to all left siblings (low precedence) + // s jazz . fast 2 => fast (s jazz) 2 + let leftSide = children.slice(0, pipeIndex); + if (leftSide.length === 1) { + leftSide = leftSide[0]; + } else { + // wrap in (..) if multiple items on the left side + leftSide = { type: 'list', children: leftSide }; + } + children = [right, leftSide, ...rightSide]; + } + } + return children; + } + parse_pair(open_type, close_type) { + this.consume(open_type); + const children = []; + while (this.tokens[0]?.type !== close_type) { + children.push(this.parse_expr()); + } + this.consume(close_type); + return children; + } + parse_list() { + let children = this.parse_pair('open_list', 'close_list'); + children = this.desugar_children(children); + return { type: 'list', children }; + } + parse_cat() { + let children = this.parse_pair('open_cat', 'close_cat'); + children = [{ type: 'plain', value: 'cat' }, ...children]; + children = this.desugar_children(children); + children = this.desugar_stack(children, 'cat'); + return { type: 'list', children }; + } + parse_seq() { + let children = this.parse_pair('open_seq', 'close_seq'); + children = [{ type: 'plain', value: 'seq' }, ...children]; + children = this.desugar_children(children); + children = this.desugar_stack(children, 'seq'); + return { type: 'list', children }; + } + consume(type) { + // shift removes first element and returns it + const token = this.tokens.shift(); + if (token.type !== type) { + throw new Error(`expected token type ${type}, got ${token.type}`); + } + return token; + } +} + +export function printAst(ast, compact = false, lvl = 0) { + const br = compact ? '' : '\n'; + const spaces = compact ? '' : Array(lvl).fill(' ').join(''); + if (ast.type === 'list') { + return `${lvl ? br : ''}${spaces}(${ast.children.map((child) => printAst(child, compact, lvl + 1)).join(' ')}${ + ast.children.find((child) => child.type === 'list') ? `${br}${spaces})` : ')' + }`; + } + return `${ast.value}`; +} + +// lisp runner +export class UzuRunner { + constructor(lib) { + this.parser = new UzuParser(); + this.lib = lib; + } + // a helper to check conditions and throw if they are not met + assert(condition, error) { + if (!condition) { + throw new Error(error); + } + } + run(code) { + const ast = this.parser.parse(code); + return this.call(ast); + } + call(ast) { + // for a node to be callable, it needs to be a list + this.assert(ast.type === 'list', `function call: expected list, got ${ast.type}`); + // the first element is expected to be the function name + this.assert(ast.children[0]?.type === 'plain', `function call: expected first child to be plain, got ${ast.type}`); + + // process args + const args = ast.children.slice(1).map((arg) => { + if (arg.type === 'string') { + return this.lib.string(arg.value.slice(1, -1)); + } + if (arg.type === 'plain') { + return this.lib.plain(arg.value); + } + if (arg.type === 'number') { + return this.lib.number(Number(arg.value)); + } + return this.call(arg); + }); + + const name = ast.children[0].value; + if (name === '.') { + // lambda : (.fast 2) = x=>fast(2, x) + const callee = ast.children[1].value; + const innerFn = this.lib[callee]; + this.assert(innerFn, `function call: unknown function name "${callee}"`); + return (pat) => innerFn(pat, args.slice(1)); + } + + // look up function in lib + const fn = this.lib[name]; + this.assert(fn, `function call: unknown function name "${name}"`); + return fn(...args); + } +} diff --git a/packages/uzu/vite.config.js b/packages/uzu/vite.config.js new file mode 100644 index 000000000..e61a69e40 --- /dev/null +++ b/packages/uzu/vite.config.js @@ -0,0 +1,19 @@ +import { defineConfig } from 'vite'; +//import { dependencies } from './package.json'; +import { resolve } from 'path'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [], + build: { + lib: { + entry: resolve(__dirname, 'uzu.mjs'), + formats: ['es'], + fileName: (ext) => ({ es: 'uzu.mjs' })[ext], + }, + rollupOptions: { + // external: [...Object.keys(dependencies)], + }, + target: 'esnext', + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 32fd711e2..bbe64f07a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,7 +41,7 @@ importers: 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)) + version: 3.0.4(vitest@3.0.4) '@vitest/ui': specifier: ^3.0.4 version: 3.0.4(vitest@3.0.4) @@ -540,6 +540,15 @@ importers: specifier: ^3.0.4 version: 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) + packages/uzu: + devDependencies: + vite: + specifier: ^6.0.11 + version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0) + vitest: + specifier: ^3.0.4 + version: 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) + packages/web: dependencies: '@strudel/core': @@ -7561,7 +7570,6 @@ 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==} @@ -10248,7 +10256,7 @@ 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))': + '@vitest/coverage-v8@3.0.4(vitest@3.0.4)': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 From dbe3915368e0404e56523adddaa18038ca0cac49 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 15 Mar 2025 10:35:22 +0100 Subject: [PATCH 007/174] fix: lint errors --- packages/uzu/uzu.mjs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/uzu/uzu.mjs b/packages/uzu/uzu.mjs index ead760be0..6a5157811 100644 --- a/packages/uzu/uzu.mjs +++ b/packages/uzu/uzu.mjs @@ -8,18 +8,18 @@ This program is free software: you can redistribute it and/or modify it under th export class UzuParser { // these are the tokens we expect token_types = { - string: /^\"(.*?)\"/, + string: /^"(.*?)"/, open_list: /^\(/, close_list: /^\)/, - open_cat: /^\/, + open_cat: /^/, open_seq: /^\[/, close_seq: /^\]/, number: /^[0-9]*\.?[0-9]+/, // before pipe! pipe: /^\./, - stack: /^\,/, - op: /^[\*\/]/, - plain: /^[a-zA-Z0-9\-]+/, + stack: /^,/, + op: /^[*/]/, + plain: /^[a-zA-Z0-9-]+/, }; // matches next token next_token(code) { From b71b1354c3c8dcdfe6b8c79d1a170c9d1ae3759e Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 16 Mar 2025 00:51:59 +0100 Subject: [PATCH 008/174] uzu: stacks now work within () + write more tests --- packages/uzu/test/uzu.test.mjs | 23 +++++++++++++++++++++-- packages/uzu/uzu.mjs | 31 ++++++++++++++++++++----------- 2 files changed, 41 insertions(+), 13 deletions(-) diff --git a/packages/uzu/test/uzu.test.mjs b/packages/uzu/test/uzu.test.mjs index c58eb1a86..33451f0cf 100644 --- a/packages/uzu/test/uzu.test.mjs +++ b/packages/uzu/test/uzu.test.mjs @@ -58,13 +58,32 @@ let desguar = (a) => { describe('uzu sugar', () => { it('should desugar []', () => expect(desguar('[a b c]')).toEqual('(seq a b c)')); - it('should desugar [] nested', () => expect(desguar('[a [b c]]')).toEqual('(seq a (seq b c))')); + it('should desugar [] nested', () => expect(desguar('[a [b c] d]')).toEqual('(seq a (seq b c) d)')); it('should desugar <>', () => expect(desguar('')).toEqual('(cat a b c)')); - it('should desugar <> nested', () => expect(desguar('>')).toEqual('(cat a (cat b c))')); + it('should desugar <> nested', () => expect(desguar(' d>')).toEqual('(cat a (cat b c) d)')); it('should desugar mixed [] <>', () => expect(desguar('[a ]')).toEqual('(seq a (cat b c))')); it('should desugar mixed <> []', () => expect(desguar('')).toEqual('(cat a (seq b c))')); + it('should desugar .', () => expect(desguar('s jazz . fast 2')).toEqual('(fast (s jazz) 2)')); + it('should desugar . seq', () => expect(desguar('[bd cp . fast 2]')).toEqual('(fast (seq bd cp) 2)')); it('should desugar . twice', () => expect(desguar('s jazz . fast 2 . slow 2')).toEqual('(slow (fast (s jazz) 2) 2)')); + it('should desugar . nested', () => expect(desguar('(s cp . fast 2)')).toEqual('(fast (s cp) 2)')); + it('should desugar . within []', () => expect(desguar('[bd cp . fast 2]')).toEqual('(fast (seq bd cp) 2)')); + it('should desugar . within , within []', () => + expect(desguar('[bd cp . fast 2, x]')).toEqual('(stack (fast (seq bd cp) 2) x)')); + + it('should desugar . ()', () => expect(desguar('[jazz hh.(fast 2)]')).toEqual('(seq jazz (fast hh 2))')); + + it('should desugar , seq', () => expect(desguar('[bd, hh]')).toEqual('(stack bd hh)')); + it('should desugar , seq 2', () => expect(desguar('[bd, hh oh]')).toEqual('(stack bd (seq hh oh))')); + it('should desugar , seq 3', () => expect(desguar('[bd cp, hh oh]')).toEqual('(stack (seq bd cp) (seq hh oh))')); + it('should desugar , cat', () => expect(desguar('')).toEqual('(stack bd hh)')); + it('should desugar , cat 2', () => expect(desguar('')).toEqual('(stack bd (cat hh oh))')); + it('should desugar , cat 3', () => expect(desguar('')).toEqual('(stack (cat bd cp) (cat hh oh))')); + it('should desugar , ()', () => expect(desguar('(s bd, s cp)')).toEqual('(stack (s bd) (s cp))')); + it('should desugar * /', () => expect(desguar('[a b*2 c d/3 e]')).toEqual('(seq a (* b 2) c (/ d 3) e)')); + it('should desugar []*x', () => expect(desguar('[a [b c]*3]')).toEqual('(seq a (* (seq b c) 3))')); + it('should desugar README example', () => expect(desguar('s [bd hh*2 cp.(crush 4) ] . speed .8')).toEqual( '(speed (s (seq bd (* hh 2) (crush cp 4) (cat mt ht lt))) .8)', diff --git a/packages/uzu/uzu.mjs b/packages/uzu/uzu.mjs index 6a5157811..e79488a59 100644 --- a/packages/uzu/uzu.mjs +++ b/packages/uzu/uzu.mjs @@ -86,10 +86,14 @@ export class UzuParser { return children; } // Token[] => Token[][] . returns empty list if type not found - split_children(children, type) { + split_children(children, split_type, sequence_type) { + if (sequence_type) { + // if given, the first child is ignored + children = children.slice(1); + } const chunks = []; while (true) { - let commaIndex = children.findIndex((child) => child.type === type); + let commaIndex = children.findIndex((child) => child.type === split_type); if (commaIndex === -1) break; const chunk = children.slice(0, commaIndex); chunks.push(chunk); @@ -101,13 +105,11 @@ export class UzuParser { chunks.push(children); return chunks; } - desugar_stack(children) { - let [type, ...rest] = children; + desugar_stack(children, sequence_type) { // children is expected to contain seq or cat as first item - const chunks = this.split_children(rest, 'stack'); + const chunks = this.split_children(children, 'stack', sequence_type); if (!chunks.length) { - // no stack - return children; + return this.desugar_children(children); } // collect args of stack function const args = chunks.map((chunk) => { @@ -115,8 +117,14 @@ export class UzuParser { // chunks of one element can be added to the stack as is return chunk[0]; } else { - // chunks of multiple args are added to a subsequence of type - return { type: 'list', children: [type, ...chunk] }; + // chunks of multiple args + if (sequence_type) { + // if given, each chunk needs to be prefixed + // [a b, c d] => (stack (seq a b) (seq c d)) + chunk = [{ type: 'plain', value: sequence_type }, ...chunk]; + } + chunk = this.desugar_children(chunk); + return { type: 'list', children: chunk }; } }); return [{ type: 'plain', value: 'stack' }, ...args]; @@ -203,21 +211,22 @@ export class UzuParser { } parse_list() { let children = this.parse_pair('open_list', 'close_list'); + children = this.desugar_stack(children); children = this.desugar_children(children); return { type: 'list', children }; } parse_cat() { let children = this.parse_pair('open_cat', 'close_cat'); children = [{ type: 'plain', value: 'cat' }, ...children]; - children = this.desugar_children(children); children = this.desugar_stack(children, 'cat'); + children = this.desugar_children(children); return { type: 'list', children }; } parse_seq() { let children = this.parse_pair('open_seq', 'close_seq'); children = [{ type: 'plain', value: 'seq' }, ...children]; - children = this.desugar_children(children); children = this.desugar_stack(children, 'seq'); + children = this.desugar_children(children); return { type: 'list', children }; } consume(type) { From e3df504423b51e29d4b7ca11199cac6641d9f7a0 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 16 Mar 2025 02:01:16 +0100 Subject: [PATCH 009/174] rename uzu to mondo --- packages/{uzu => mondo}/README.md | 8 ++++---- packages/{uzu/uzu.mjs => mondo/mondo.mjs} | 8 ++++---- packages/{uzu => mondo}/package.json | 10 +++++----- .../uzu.test.mjs => mondo/test/mondo.test.mjs} | 10 +++++----- packages/{uzu => mondo}/vite.config.js | 4 ++-- pnpm-lock.yaml | 18 +++++++++--------- 6 files changed, 29 insertions(+), 29 deletions(-) rename packages/{uzu => mondo}/README.md (72%) rename packages/{uzu/uzu.mjs => mondo/mondo.mjs} (98%) rename packages/{uzu => mondo}/package.json (79%) rename packages/{uzu/test/uzu.test.mjs => mondo/test/mondo.test.mjs} (95%) rename packages/{uzu => mondo}/vite.config.js (78%) diff --git a/packages/uzu/README.md b/packages/mondo/README.md similarity index 72% rename from packages/uzu/README.md rename to packages/mondo/README.md index 53eef51f8..d7ee20ac4 100644 --- a/packages/uzu/README.md +++ b/packages/mondo/README.md @@ -1,4 +1,4 @@ -# uzu +# mondo an experimental parser for an *uzulang*, a custom dsl for patterns that can stand on its own feet. more info: @@ -6,9 +6,9 @@ an experimental parser for an *uzulang*, a custom dsl for patterns that can stan - [uzulang II](https://garten.salat.dev/uzu/uzulang2.html) ```js -import { UzuRunner } from 'uzu' +import { MondoRunner } from 'uzu' -const runner = UzuRunner({ seq, cat, s, crush, speed, '*': fast }); +const runner = MondoRunner({ seq, cat, s, crush, speed, '*': fast }); const pat = runner.run('s [bd hh*2 cp.(crush 4) ] . speed .8') ``` @@ -26,4 +26,4 @@ the above code will create the following call structure: ) ``` -you can pass all available functions to *UzuRunner* as an object. +you can pass all available functions to *MondoRunner* as an object. diff --git a/packages/uzu/uzu.mjs b/packages/mondo/mondo.mjs similarity index 98% rename from packages/uzu/uzu.mjs rename to packages/mondo/mondo.mjs index e79488a59..2037cce7c 100644 --- a/packages/uzu/uzu.mjs +++ b/packages/mondo/mondo.mjs @@ -1,11 +1,11 @@ /* -uzu.mjs - +mondo.mjs - Copyright (C) 2022 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ // evolved from https://garten.salat.dev/lisp/parser.html -export class UzuParser { +export class MondoParser { // these are the tokens we expect token_types = { string: /^"(.*?)"/, @@ -251,9 +251,9 @@ export function printAst(ast, compact = false, lvl = 0) { } // lisp runner -export class UzuRunner { +export class MondoRunner { constructor(lib) { - this.parser = new UzuParser(); + this.parser = new MondoParser(); this.lib = lib; } // a helper to check conditions and throw if they are not met diff --git a/packages/uzu/package.json b/packages/mondo/package.json similarity index 79% rename from packages/uzu/package.json rename to packages/mondo/package.json index 9b59078d5..d8a4a0bf2 100644 --- a/packages/uzu/package.json +++ b/packages/mondo/package.json @@ -1,11 +1,11 @@ { - "name": "uzu", + "name": "mondo", "version": "1.1.0", - "description": "an uzu notation", - "main": "uzu.mjs", + "description": "a language for functional composition that translates to js", + "main": "mondo.mjs", "type": "module", "publishConfig": { - "main": "dist/uzu.mjs" + "main": "dist/mondo.mjs" }, "scripts": { "test": "vitest run", @@ -29,7 +29,7 @@ "bugs": { "url": "https://github.com/tidalcycles/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel/blob/main/packages/uzu/README.md", + "homepage": "https://github.com/tidalcycles/strudel/blob/main/packages/mondo/README.md", "devDependencies": { "vite": "^6.0.11", "vitest": "^3.0.4" diff --git a/packages/uzu/test/uzu.test.mjs b/packages/mondo/test/mondo.test.mjs similarity index 95% rename from packages/uzu/test/uzu.test.mjs rename to packages/mondo/test/mondo.test.mjs index 33451f0cf..6ed2df176 100644 --- a/packages/uzu/test/uzu.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -1,16 +1,16 @@ /* -uzu.test.mjs - +mondo.test.mjs - Copyright (C) 2022 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ import { describe, expect, it } from 'vitest'; -import { UzuParser, UzuRunner, printAst } from '../uzu.mjs'; +import { MondoParser, MondoRunner, printAst } from '../mondo.mjs'; -const parser = new UzuParser(); +const parser = new MondoParser(); const p = (code) => parser.parse(code); -describe('uzu s-expressions parser', () => { +describe('mondo s-expressions parser', () => { it('should parse an empty string', () => expect(p('')).toEqual({ type: 'list', children: [] })); it('should parse a single item', () => expect(p('a')).toEqual({ type: 'list', children: [{ type: 'plain', value: 'a' }] })); @@ -56,7 +56,7 @@ let desguar = (a) => { return printAst(parser.parse(a), true); }; -describe('uzu sugar', () => { +describe('mondo sugar', () => { it('should desugar []', () => expect(desguar('[a b c]')).toEqual('(seq a b c)')); it('should desugar [] nested', () => expect(desguar('[a [b c] d]')).toEqual('(seq a (seq b c) d)')); it('should desugar <>', () => expect(desguar('')).toEqual('(cat a b c)')); diff --git a/packages/uzu/vite.config.js b/packages/mondo/vite.config.js similarity index 78% rename from packages/uzu/vite.config.js rename to packages/mondo/vite.config.js index e61a69e40..19e53d7f3 100644 --- a/packages/uzu/vite.config.js +++ b/packages/mondo/vite.config.js @@ -7,9 +7,9 @@ export default defineConfig({ plugins: [], build: { lib: { - entry: resolve(__dirname, 'uzu.mjs'), + entry: resolve(__dirname, 'mondo.mjs'), formats: ['es'], - fileName: (ext) => ({ es: 'uzu.mjs' })[ext], + fileName: (ext) => ({ es: 'mondo.mjs' })[ext], }, rollupOptions: { // external: [...Object.keys(dependencies)], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bbe64f07a..264b7440b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -344,6 +344,15 @@ importers: specifier: ^3.0.4 version: 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) + packages/mondo: + devDependencies: + vite: + specifier: ^6.0.11 + version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0) + vitest: + specifier: ^3.0.4 + version: 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) + packages/motion: dependencies: '@strudel/core': @@ -540,15 +549,6 @@ importers: specifier: ^3.0.4 version: 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) - packages/uzu: - devDependencies: - vite: - specifier: ^6.0.11 - version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0) - vitest: - specifier: ^3.0.4 - version: 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) - packages/web: dependencies: '@strudel/core': From e782dc09dd3c31fb497d1f66a50bfa59924af8bb Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 16 Mar 2025 10:47:32 +0100 Subject: [PATCH 010/174] export strudelScope for mondo to use --- packages/core/evaluate.mjs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/core/evaluate.mjs b/packages/core/evaluate.mjs index e3e73d596..7d57e435e 100644 --- a/packages/core/evaluate.mjs +++ b/packages/core/evaluate.mjs @@ -4,6 +4,8 @@ Copyright (C) 2022 Strudel contributors - see . */ +export const strudelScope = {}; + export const evalScope = async (...args) => { const results = await Promise.allSettled(args); const modules = results.filter((result) => result.status === 'fulfilled').map((r) => r.value); @@ -18,6 +20,7 @@ export const evalScope = async (...args) => { modules.forEach((module) => { Object.entries(module).forEach(([name, value]) => { globalThis[name] = value; + strudelScope[name] = value; }); }); return modules; From 11196fb1e6601241039f2dfd4dca6c4434ad7a1a Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 16 Mar 2025 10:48:38 +0100 Subject: [PATCH 011/174] breaking: refactor controls - multiple args wont be interpreted as sequence anymore - you can now pass value, pat to set value to pat --- packages/core/controls.mjs | 26 ++++++++++++++++------- packages/core/signal.mjs | 8 +++---- packages/core/test/pattern.test.mjs | 2 +- packages/tonal/test/tonal.test.mjs | 14 ++++++------ test/__snapshots__/examples.test.mjs.snap | 16 ++++---------- website/src/repl/tunes.mjs | 12 +++++------ 6 files changed, 40 insertions(+), 38 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 1f2d3e703..dbf1255e8 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -4,13 +4,14 @@ Copyright (C) 2022 Strudel contributors - see . */ -import { Pattern, register, sequence } from './pattern.mjs'; +import { Pattern, register, reify } from './pattern.mjs'; export function createParam(names) { let isMulti = Array.isArray(names); names = !isMulti ? [names] : names; const name = names[0]; + // todo: make this less confusing const withVal = (xs) => { let bag; // check if we have an object with an unnamed control (.value) @@ -35,25 +36,34 @@ export function createParam(names) { } }; - const func = (...pats) => sequence(...pats).withValue(withVal); - - const setter = function (...pats) { - if (!pats.length) { - return this.fmap(withVal); + // todo: make this less confusing + const func = function (value, pat) { + if (!pat) { + return reify(value).withValue(withVal); } - return this.set(func(...pats)); + if (typeof value === 'undefined') { + return pat.fmap(withVal); + } + return pat.set(reify(value).withValue(withVal)); + }; + Pattern.prototype[name] = function (value) { + return func(value, this); }; - Pattern.prototype[name] = setter; return func; } // maps control alias names to the "main" control name const controlAlias = new Map(); +export function isControlName(name) { + return controlAlias.has(name); +} + export function registerControl(names, ...aliases) { const name = Array.isArray(names) ? names[0] : names; let bag = {}; bag[name] = createParam(names); + controlAlias.set(name, name); aliases.forEach((alias) => { bag[alias] = bag[name]; controlAlias.set(alias, name); diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index 5348173d3..aa8dd9a6b 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -279,26 +279,26 @@ const _rearrangeWith = (ipat, n, pat) => { }; /** - * @name shuffle * Slices a pattern into the given number of parts, then plays those parts in random order. * Each part will be played exactly once per cycle. + * @name shuffle * @example * note("c d e f").sound("piano").shuffle(4) * @example - * note("c d e f".shuffle(4), "g").sound("piano") + * seq("c d e f".shuffle(4), "g").note().sound("piano") */ export const shuffle = register('shuffle', (n, pat) => { return _rearrangeWith(randrun(n), n, pat); }); /** - * @name scramble * Slices a pattern into the given number of parts, then plays those parts at random. Similar to `shuffle`, * but parts might be played more than once, or not at all, per cycle. + * @name scramble * @example * note("c d e f").sound("piano").scramble(4) * @example - * note("c d e f".scramble(4), "g").sound("piano") + * seq("c d e f".scramble(4), "g").note().sound("piano") */ export const scramble = register('scramble', (n, pat) => { return _rearrangeWith(_irand(n)._segment(n), n, pat); diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs index 08611032c..ee6ce197f 100644 --- a/packages/core/test/pattern.test.mjs +++ b/packages/core/test/pattern.test.mjs @@ -1001,7 +1001,7 @@ describe('Pattern', () => { }); describe('hurry', () => { it('Can speed up patterns and sounds', () => { - sameFirst(s('a', 'b').hurry(2), s('a', 'b').fast(2).speed(2)); + sameFirst(s(sequence('a', 'b')).hurry(2), s(sequence('a', 'b')).fast(2).speed(2)); }); }); /*describe('composable functions', () => { diff --git a/packages/tonal/test/tonal.test.mjs b/packages/tonal/test/tonal.test.mjs index 8dd0e1861..3a5d920e5 100644 --- a/packages/tonal/test/tonal.test.mjs +++ b/packages/tonal/test/tonal.test.mjs @@ -25,28 +25,28 @@ describe('tonal', () => { }); it('scale with n values', () => { expect( - n(0, 1, 2) + n(seq(0, 1, 2)) .scale('C major') .firstCycleValues.map((h) => h.note), ).toEqual(['C3', 'D3', 'E3']); }); it('scale with colon', () => { expect( - n(0, 1, 2) + n(seq(0, 1, 2)) .scale('C:major') .firstCycleValues.map((h) => h.note), ).toEqual(['C3', 'D3', 'E3']); }); it('scale with mininotation colon', () => { expect( - n(0, 1, 2) + n(seq(0, 1, 2)) .scale(mini('C:major')) .firstCycleValues.map((h) => h.note), ).toEqual(['C3', 'D3', 'E3']); }); it('transposes note numbers with interval numbers', () => { expect( - note(40, 40, 40) + note(seq(40, 40, 40)) .transpose(0, 1, 2) .firstCycleValues.map((h) => h.note), ).toEqual([40, 41, 42]); @@ -54,7 +54,7 @@ describe('tonal', () => { }); it('transposes note numbers with interval strings', () => { expect( - note(40, 40, 40) + note(seq(40, 40, 40)) .transpose('1P', '2M', '3m') .firstCycleValues.map((h) => h.note), ).toEqual([40, 42, 43]); @@ -62,7 +62,7 @@ describe('tonal', () => { }); it('transposes note strings with interval numbers', () => { expect( - note('c', 'c', 'c') + note(seq('c', 'c', 'c')) .transpose(0, 1, 2) .firstCycleValues.map((h) => h.note), ).toEqual(['C', 'Db', 'D']); @@ -70,7 +70,7 @@ describe('tonal', () => { }); it('transposes note strings with interval strings', () => { expect( - note('c', 'c', 'c') + note(seq('c', 'c', 'c')) .transpose('1P', '2M', '3m') .firstCycleValues.map((h) => h.note), ).toEqual(['C', 'D', 'Eb']); diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 96a11d49b..93ca7ff68 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -7401,9 +7401,7 @@ exports[`runs examples > example "scope" example index 0 1`] = ` ] `; -exports[`runs examples > example "scramble -Slices a pattern into the given number of parts, then plays those parts at random. Similar to \`shuffle\`, -but parts might be played more than once, or not at all, per cycle." example index 0 1`] = ` +exports[`runs examples > example "scramble" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:c s:piano ]", "[ 1/4 → 1/2 | note:d s:piano ]", @@ -7424,9 +7422,7 @@ but parts might be played more than once, or not at all, per cycle." example ind ] `; -exports[`runs examples > example "scramble -Slices a pattern into the given number of parts, then plays those parts at random. Similar to \`shuffle\`, -but parts might be played more than once, or not at all, per cycle." example index 1 1`] = ` +exports[`runs examples > example "scramble" example index 1 1`] = ` [ "[ 0/1 → 1/8 | note:c s:piano ]", "[ 1/8 → 1/4 | note:d s:piano ]", @@ -7835,9 +7831,7 @@ exports[`runs examples > example "shrink" example index 3 1`] = ` ] `; -exports[`runs examples > example "shuffle -Slices a pattern into the given number of parts, then plays those parts in random order. -Each part will be played exactly once per cycle." example index 0 1`] = ` +exports[`runs examples > example "shuffle" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:c s:piano ]", "[ 1/4 → 1/2 | note:d s:piano ]", @@ -7858,9 +7852,7 @@ Each part will be played exactly once per cycle." example index 0 1`] = ` ] `; -exports[`runs examples > example "shuffle -Slices a pattern into the given number of parts, then plays those parts in random order. -Each part will be played exactly once per cycle." example index 1 1`] = ` +exports[`runs examples > example "shuffle" example index 1 1`] = ` [ "[ 0/1 → 1/8 | note:c s:piano ]", "[ 1/8 → 1/4 | note:d s:piano ]", diff --git a/website/src/repl/tunes.mjs b/website/src/repl/tunes.mjs index cffe0c0eb..4e2a7617f 100644 --- a/website/src/repl/tunes.mjs +++ b/website/src/repl/tunes.mjs @@ -69,32 +69,32 @@ stack( export const giantSteps = `// John Coltrane - Giant Steps -let melody = note( +let melody = seq( "[F#5 D5] [B4 G4] Bb4 [B4 A4]", "[D5 Bb4] [G4 Eb4] F#4 [G4 F4]", "Bb4 [B4 A4] D5 [D#5 C#5]", "F#5 [G5 F5] Bb5 [F#5 F#5]", -) +).note() stack( // melody melody.color('#F8E71C'), // chords - chord( + seq( "[B^7 D7] [G^7 Bb7] Eb^7 [Am7 D7]", "[G^7 Bb7] [Eb^7 F#7] B^7 [Fm7 Bb7]", "Eb^7 [Am7 D7] G^7 [C#m7 F#7]", "B^7 [Fm7 Bb7] Eb^7 [C#m7 F#7]" - ).dict('lefthand') + ).chord().dict('lefthand') .anchor(melody).mode('duck') .voicing().color('#7ED321'), // bass - note( + seq( "[B2 D2] [G2 Bb2] [Eb2 Bb3] [A2 D2]", "[G2 Bb2] [Eb2 F#2] [B2 F#2] [F2 Bb2]", "[Eb2 Bb2] [A2 D2] [G2 D2] [C#2 F#2]", "[B2 F#2] [F2 Bb2] [Eb2 Bb3] [C#2 F#2]" - ).color('#00B8D4') + ).note().color('#00B8D4') ).slow(20) .pianoroll({fold:1})`; From fae59a6bcd1624f951737b1aa2f145879069f2c4 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 16 Mar 2025 10:49:18 +0100 Subject: [PATCH 012/174] feat: add mondough package to run strudel via mondo --- packages/mondo/mondo.mjs | 22 +++++-- packages/mondough/README.md | 3 + packages/mondough/mondough.mjs | 35 +++++++++++ packages/mondough/package.json | 42 +++++++++++++ packages/mondough/vite.config.js | 19 ++++++ pnpm-lock.yaml | 103 +++++++++++++++++++++++++++++++ website/package.json | 1 + website/src/repl/util.mjs | 1 + 8 files changed, 221 insertions(+), 5 deletions(-) create mode 100644 packages/mondough/README.md create mode 100644 packages/mondough/mondough.mjs create mode 100644 packages/mondough/package.json create mode 100644 packages/mondough/vite.config.js diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 2037cce7c..907951db2 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -19,8 +19,11 @@ export class MondoParser { pipe: /^\./, stack: /^,/, op: /^[*/]/, - plain: /^[a-zA-Z0-9-]+/, + plain: /^[a-zA-Z0-9-_]+/, }; + constructor(config) { + this.config = config; + } // matches next token next_token(code) { for (let type in this.token_types) { @@ -29,7 +32,7 @@ export class MondoParser { return { type, value: match[0] }; } } - throw new Error(`zilp: could not match '${code}'`); + throw new Error(`mondo: could not match '${code}'`); } // takes code string, returns list of matched tokens (if valid) tokenize(code) { @@ -182,7 +185,7 @@ export class MondoParser { const [callee, ...rest] = right.children; const leftSide = children.slice(0, pipeIndex - 1); const left = children[pipeIndex - 1]; - const args = [callee, left, ...rest]; + let args = [callee, left, ...rest]; const call = { type: 'list', children: args }; children = [...leftSide, call, ...rightSide]; } else { @@ -200,6 +203,10 @@ export class MondoParser { } return children; } + flip_call(children) { + let [name, first, ...rest] = children; + return [name, ...rest, first]; + } parse_pair(open_type, close_type) { this.consume(open_type); const children = []; @@ -252,9 +259,10 @@ export function printAst(ast, compact = false, lvl = 0) { // lisp runner export class MondoRunner { - constructor(lib) { - this.parser = new MondoParser(); + constructor(lib, config = {}) { + this.parser = new MondoParser(config); this.lib = lib; + this.config = config; } // a helper to check conditions and throw if they are not met assert(condition, error) { @@ -264,6 +272,7 @@ export class MondoRunner { } run(code) { const ast = this.parser.parse(code); + console.log(printAst(ast)); return this.call(ast); } call(ast) { @@ -298,6 +307,9 @@ export class MondoRunner { // look up function in lib const fn = this.lib[name]; this.assert(fn, `function call: unknown function name "${name}"`); + if (this.lib.call) { + return this.lib.call(fn, args, name); + } return fn(...args); } } diff --git a/packages/mondough/README.md b/packages/mondough/README.md new file mode 100644 index 000000000..3ae758912 --- /dev/null +++ b/packages/mondough/README.md @@ -0,0 +1,3 @@ +# @strudel/mondough + +connects mondo to strudel. diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs new file mode 100644 index 000000000..1d29749f9 --- /dev/null +++ b/packages/mondough/mondough.mjs @@ -0,0 +1,35 @@ +import { strudelScope, reify, fast, slow, isControlName } from '@strudel/core'; +import { MondoRunner } from '../mondo/mondo.mjs'; + +let runner = new MondoRunner(strudelScope, { pipepost: true }); + +//strudelScope.plain = reify; +strudelScope.plain = (v) => { + // console.log('plain', v); + return reify(v); + // return v; +}; +// strudelScope.number = (n) => n; +strudelScope.number = reify; + +strudelScope.call = (fn, args, name) => { + const [pat, ...rest] = args; + if (!['seq', 'cat', 'stack'].includes(name)) { + args = [...rest, pat]; + } + + // console.log('call', name, ...flipped); + + return fn(...args); +}; + +strudelScope['*'] = fast; +strudelScope['/'] = slow; + +export function mondo(code, offset = 0) { + if (Array.isArray(code)) { + code = code.join(''); + } + const pat = runner.run(code, { pipepost: true }); + return pat; +} diff --git a/packages/mondough/package.json b/packages/mondough/package.json new file mode 100644 index 000000000..0754a697f --- /dev/null +++ b/packages/mondough/package.json @@ -0,0 +1,42 @@ +{ + "name": "@strudel/mondough", + "version": "1.1.0", + "description": "mondo notation for strudel", + "main": "mondough.mjs", + "type": "module", + "publishConfig": { + "main": "dist/mondough.mjs" + }, + "scripts": { + "test": "vitest run", + "bench": "vitest bench", + "build:parser": "peggy -o krill-parser.js --format es ./krill.pegjs", + "build": "vite build", + "prepublishOnly": "npm run build" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/tidalcycles/strudel.git" + }, + "keywords": [ + "tidalcycles", + "strudel", + "pattern", + "livecoding", + "algorave" + ], + "author": "Felix Roos ", + "license": "AGPL-3.0-or-later", + "bugs": { + "url": "https://github.com/tidalcycles/strudel/issues" + }, + "homepage": "https://github.com/tidalcycles/strudel#readme", + "dependencies": { + "@strudel/core": "workspace:*" + }, + "devDependencies": { + "mondo": "*", + "vite": "^6.0.11", + "vitest": "^3.0.4" + } +} diff --git a/packages/mondough/vite.config.js b/packages/mondough/vite.config.js new file mode 100644 index 000000000..c46972e96 --- /dev/null +++ b/packages/mondough/vite.config.js @@ -0,0 +1,19 @@ +import { defineConfig } from 'vite'; +//import { dependencies } from './package.json'; +import { resolve } from 'path'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [], + build: { + lib: { + entry: resolve(__dirname, 'mondough.mjs'), + formats: ['es'], + fileName: (ext) => ({ es: 'mondough.mjs' })[ext], + }, + rollupOptions: { + // external: [...Object.keys(dependencies)], + }, + target: 'esnext', + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 264b7440b..f5b63c56f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -353,6 +353,22 @@ importers: specifier: ^3.0.4 version: 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) + packages/mondough: + dependencies: + '@strudel/core': + specifier: workspace:* + version: link:../core + devDependencies: + mondo: + specifier: '*' + version: 0.4.4 + vite: + specifier: ^6.0.11 + version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0) + vitest: + specifier: ^3.0.4 + version: 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) + packages/motion: dependencies: '@strudel/core': @@ -674,6 +690,9 @@ importers: '@strudel/mini': specifier: workspace:* version: link:../packages/mini + '@strudel/mondough': + specifier: workspace:* + version: link:../packages/mondough '@strudel/motion': specifier: workspace:* version: link:../packages/motion @@ -2963,6 +2982,10 @@ packages: resolution: {integrity: sha512-groO71Fvi5SWpxjI9Ia+chy0QBwT61mg6yxJV27f5YFf+Mw+STT75K6SHySpP8Co5LsCrtsbCH5dJZSRtkSKaQ==} engines: {node: '>= 14.0.0'} + amdefine@1.0.1: + resolution: {integrity: sha512-S2Hw0TtNkMJhIabBwIojKL9YHO5T0n5eNqWJ7Lrlel/zDbftQpxpapi8tZs3X1HWa+u+QeydGmzzNU0m09+Rcg==} + engines: {node: '>=0.4.2'} + ansi-align@3.0.1: resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} @@ -3091,6 +3114,9 @@ packages: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} engines: {node: '>= 0.4'} + async@0.2.10: + resolution: {integrity: sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ==} + async@3.2.6: resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} @@ -3325,6 +3351,9 @@ packages: claviature@0.1.0: resolution: {integrity: sha512-Ai12axNwQ7x/F9QAj64RYKsgvi5Y33+X3GUSKAC/9s/adEws8TSSc0efeiqhKNGKBo6rT/c+CSCwSXzXxwxZzQ==} + cldr-plurals@1.0.0: + resolution: {integrity: sha512-xGkehDsjj/ng1LtYiAzdiqDgqTQ/qmWafDlB6R8CfXbpXe4Ge2Tl4l7gxiA+yaD1WCTP3EVfwerdmVwfco7vxw==} + clean-stack@2.2.0: resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} engines: {node: '>=6'} @@ -4366,6 +4395,9 @@ packages: resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==} engines: {node: '>=16 || 14 >=14.17'} + globalize@0.1.1: + resolution: {integrity: sha512-5e01v8eLGfuQSOvx2MsDMOWS0GFtCx1wPzQSmcHw4hkxFzrQDBO3Xwg/m8Hr/7qXMrHeOIE29qWVzyv06u1TZA==} + globals@11.12.0: resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} engines: {node: '>=4'} @@ -4427,6 +4459,11 @@ packages: h3@1.14.0: resolution: {integrity: sha512-ao22eiONdgelqcnknw0iD645qW0s9NnrJHr5OBz4WOMdBdycfSas1EQf1wXRsm+PcB2Yoj43pjBPwqIpJQTeWg==} + handlebars@1.1.2: + resolution: {integrity: sha512-6lekK3aSHE7XnEqEs+JWQJRSRdCqJYHEVjEfWWZv9pjLUYZNeP26EtlgOrpDCSX+k5N1m74urTbztgBOCko1Kg==} + engines: {node: '>=0.4.7'} + hasBin: true + handlebars@4.7.8: resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} engines: {node: '>=0.4.7'} @@ -5574,6 +5611,10 @@ packages: engines: {node: '>=18'} hasBin: true + mondo@0.4.4: + resolution: {integrity: sha512-3ot6iKwC9KZvwmyZ9dgPCnlt0O1GuBnK8QZyhnoJdtkIiRL9X/cmQuu88CbFSznQx6bq19xQdwJHpv8hggH62Q==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + mrmime@2.0.0: resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==} engines: {node: '>=10'} @@ -5815,6 +5856,9 @@ packages: resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} engines: {node: '>=12'} + optimist@0.3.7: + resolution: {integrity: sha512-TCx0dXQzVtSCg2OgY/bO9hjM9cV4XYx09TVK+s3+FhkjT6LovsLe+pPMzpWf+6yXK/hUizs2gUoTw3jHM0VaTQ==} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -6713,6 +6757,10 @@ packages: source-map-support@0.5.21: resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + source-map@0.1.43: + resolution: {integrity: sha512-VtCvB9SIQhk3aF6h+N85EaqIaBFIAfZ9Cu+NJHHVvc8BbEcnvDcFw6sqQ2dQrT6SlOrZq3tIvyD9+EGq/lJryQ==} + engines: {node: '>=0.8.0'} + source-map@0.5.7: resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} engines: {node: '>=0.10.0'} @@ -7143,6 +7191,11 @@ packages: ufo@1.5.4: resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==} + uglify-js@2.3.6: + resolution: {integrity: sha512-T2LWWydxf5+Btpb0S/Gg/yKFmYjnX9jtQ4mdN9YRq73BhN21EhU0Dvw3wYDLqd3TooGUJlCKf3Gfyjjy/RTcWA==} + engines: {node: '>=0.4.0'} + hasBin: true + uglify-js@3.19.3: resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} engines: {node: '>=0.8.0'} @@ -7161,6 +7214,9 @@ packages: underscore@1.13.7: resolution: {integrity: sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==} + underscore@1.5.2: + resolution: {integrity: sha512-yejOFsRnTJs0N9CK5Apzf6maDO2djxGoLLrlZlvGs2o9ZQuhIhDL18rtFyy4FBIbOkzA6+4hDgXbgz5EvDQCXQ==} + undici-types@6.20.0: resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} @@ -7543,6 +7599,10 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + wordwrap@0.0.3: + resolution: {integrity: sha512-1tMA907+V4QmxV7dbRvb4/8MaRALK6q9Abid3ndMYnbyo8piisCmeONVqVSXqQA3KaP4SLt5b7ud6E2sqP8TFw==} + engines: {node: '>=0.4.0'} + wordwrap@1.0.0: resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} @@ -10426,6 +10486,9 @@ snapshots: '@algolia/requester-fetch': 5.20.0 '@algolia/requester-node-http': 5.20.0 + amdefine@1.0.1: + optional: true + ansi-align@3.0.1: dependencies: string-width: 4.2.3 @@ -10635,6 +10698,9 @@ snapshots: async-function@1.0.0: {} + async@0.2.10: + optional: true + async@3.2.6: {} asynckit@0.4.0: {} @@ -10892,6 +10958,8 @@ snapshots: claviature@0.1.0: {} + cldr-plurals@1.0.0: {} + clean-stack@2.2.0: {} cli-boxes@3.0.0: {} @@ -12034,6 +12102,8 @@ snapshots: minipass: 4.2.8 path-scurry: 1.11.1 + globalize@0.1.1: {} + globals@11.12.0: {} globals@14.0.0: {} @@ -12100,6 +12170,12 @@ snapshots: uncrypto: 0.1.3 unenv: 1.10.0 + handlebars@1.1.2: + dependencies: + optimist: 0.3.7 + optionalDependencies: + uglify-js: 2.3.6 + handlebars@4.7.8: dependencies: minimist: 1.2.8 @@ -13683,6 +13759,13 @@ snapshots: requirejs: 2.3.7 requirejs-config-file: 4.0.0 + mondo@0.4.4: + dependencies: + cldr-plurals: 1.0.0 + globalize: 0.1.1 + handlebars: 1.1.2 + underscore: 1.5.2 + mrmime@2.0.0: {} ms@2.0.0: {} @@ -13975,6 +14058,10 @@ snapshots: is-docker: 2.2.1 is-wsl: 2.2.0 + optimist@0.3.7: + dependencies: + wordwrap: 0.0.3 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -15082,6 +15169,11 @@ snapshots: buffer-from: 1.1.2 source-map: 0.6.1 + source-map@0.1.43: + dependencies: + amdefine: 1.0.1 + optional: true + source-map@0.5.7: {} source-map@0.6.1: {} @@ -15549,6 +15641,13 @@ snapshots: ufo@1.5.4: {} + uglify-js@2.3.6: + dependencies: + async: 0.2.10 + optimist: 0.3.7 + source-map: 0.1.43 + optional: true + uglify-js@3.19.3: optional: true @@ -15565,6 +15664,8 @@ snapshots: underscore@1.13.7: {} + underscore@1.5.2: {} + undici-types@6.20.0: {} unenv@1.10.0: @@ -15945,6 +16046,8 @@ snapshots: word-wrap@1.2.5: {} + wordwrap@0.0.3: {} + wordwrap@1.0.0: {} workbox-background-sync@7.0.0: diff --git a/website/package.json b/website/package.json index 257ff6430..e42493520 100644 --- a/website/package.json +++ b/website/package.json @@ -42,6 +42,7 @@ "@strudel/tonal": "workspace:*", "@strudel/transpiler": "workspace:*", "@strudel/webaudio": "workspace:*", + "@strudel/mondough": "workspace:*", "@strudel/xen": "workspace:*", "@supabase/supabase-js": "^2.48.1", "@tailwindcss/forms": "^0.5.10", diff --git a/website/src/repl/util.mjs b/website/src/repl/util.mjs index a8d184285..cb5e6f36e 100644 --- a/website/src/repl/util.mjs +++ b/website/src/repl/util.mjs @@ -84,6 +84,7 @@ export function loadModules() { import('@strudel/gamepad'), import('@strudel/motion'), import('@strudel/mqtt'), + import('@strudel/mondough'), ]; if (isTauri()) { modules = modules.concat([ From b0da353115fe1dcb96142e74710947ca008706e7 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 16 Mar 2025 21:04:05 +0100 Subject: [PATCH 013/174] mondo highlighting --- packages/mondo/mondo.mjs | 64 ++++++++++++++++++++---------- packages/mondo/test/mondo.test.mjs | 18 ++++++++- packages/mondough/mondough.mjs | 29 ++++++++------ packages/mondough/package.json | 3 +- packages/transpiler/transpiler.mjs | 62 +++++++++++++++++++++++++++-- 5 files changed, 138 insertions(+), 38 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 907951db2..be5e819fe 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -19,35 +19,46 @@ export class MondoParser { pipe: /^\./, stack: /^,/, op: /^[*/]/, - plain: /^[a-zA-Z0-9-_]+/, + plain: /^[a-zA-Z0-9-_\^]+/, }; - constructor(config) { - this.config = config; - } // matches next token - next_token(code) { + next_token(code, offset = 0) { for (let type in this.token_types) { const match = code.match(this.token_types[type]); if (match) { - return { type, value: match[0] }; + let token = { type, value: match[0] }; + if (offset !== -1) { + // add location + token.loc = [offset, offset + match[0].length]; + } + return token; } } throw new Error(`mondo: could not match '${code}'`); } // takes code string, returns list of matched tokens (if valid) - tokenize(code) { + tokenize(code, offset = 0) { let tokens = []; + let locEnabled = offset !== -1; + let trim = () => { + // trim whitespace at start, update offset + offset += code.length - code.trimStart().length; + // trim start and end to not confuse parser + return code.trim(); + }; + code = trim(); while (code.length > 0) { - code = code.trim(); - const token = this.next_token(code); + code = trim(); + const token = this.next_token(code, locEnabled ? offset : -1); code = code.slice(token.value.length); + offset += token.value.length; tokens.push(token); } return tokens; } // take code, return abstract syntax tree - parse(code) { - this.tokens = this.tokenize(code); + parse(code, offset) { + this.tokens = this.tokenize(code, offset); const expressions = []; while (this.tokens.length) { expressions.push(this.parse_expr()); @@ -244,6 +255,20 @@ export class MondoParser { } return token; } + get_locations(code, offset = 0) { + let walk = (ast, locations = []) => { + if (ast.type === 'list') { + return ast.children.slice(1).forEach((child) => walk(child, locations)); + } + if (ast.loc) { + locations.push(ast.loc); + } + }; + const ast = this.parse(code, offset); + let locations = []; + walk(ast, locations); + return locations; + } } export function printAst(ast, compact = false, lvl = 0) { @@ -259,10 +284,9 @@ export function printAst(ast, compact = false, lvl = 0) { // lisp runner export class MondoRunner { - constructor(lib, config = {}) { - this.parser = new MondoParser(config); + constructor(lib) { + this.parser = new MondoParser(); this.lib = lib; - this.config = config; } // a helper to check conditions and throw if they are not met assert(condition, error) { @@ -270,9 +294,9 @@ export class MondoRunner { throw new Error(error); } } - run(code) { - const ast = this.parser.parse(code); - console.log(printAst(ast)); + run(code, offset = 0) { + const ast = this.parser.parse(code, offset); + // console.log(printAst(ast)); return this.call(ast); } call(ast) { @@ -284,13 +308,13 @@ export class MondoRunner { // process args const args = ast.children.slice(1).map((arg) => { if (arg.type === 'string') { - return this.lib.string(arg.value.slice(1, -1)); + return this.lib.string(arg.value.slice(1, -1), arg); } if (arg.type === 'plain') { - return this.lib.plain(arg.value); + return this.lib.plain(arg.value, arg); } if (arg.type === 'number') { - return this.lib.number(Number(arg.value)); + return this.lib.number(Number(arg.value), arg); } return this.call(arg); }); diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index 6ed2df176..40595004d 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -8,8 +8,24 @@ import { describe, expect, it } from 'vitest'; import { MondoParser, MondoRunner, printAst } from '../mondo.mjs'; const parser = new MondoParser(); -const p = (code) => parser.parse(code); +const p = (code) => parser.parse(code, -1); +describe('mondo tokenizer', () => { + const parser = new MondoParser(); + it('should tokenize with locations', () => + expect( + parser + .tokenize('(one two three)') + .map((t) => t.value + '=' + t.loc.join('-')) + .join(' '), + ).toEqual('(=0-1 one=1-4 two=5-8 three=9-14 )=14-15')); + // it('should parse with locations', () => expect(parser.parse('(one two three)')).toEqual()); + it('should get locations', () => + expect(parser.get_locations('s bd rim')).toEqual([ + [2, 4], + [5, 8], + ])); +}); describe('mondo s-expressions parser', () => { it('should parse an empty string', () => expect(p('')).toEqual({ type: 'list', children: [] })); it('should parse a single item', () => diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index 1d29749f9..03437be64 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -1,25 +1,22 @@ -import { strudelScope, reify, fast, slow, isControlName } from '@strudel/core'; +import { strudelScope, reify, fast, slow } from '@strudel/core'; +import { registerLanguage } from '@strudel/transpiler'; import { MondoRunner } from '../mondo/mondo.mjs'; -let runner = new MondoRunner(strudelScope, { pipepost: true }); +let runner = new MondoRunner(strudelScope, { pipepost: true, loc: true }); -//strudelScope.plain = reify; -strudelScope.plain = (v) => { - // console.log('plain', v); - return reify(v); - // return v; +let getLeaf = (value, token) => { + const [from, to] = token.loc; + return reify(value).withLoc(from, to); }; -// strudelScope.number = (n) => n; -strudelScope.number = reify; + +strudelScope.plain = getLeaf; +strudelScope.number = getLeaf; strudelScope.call = (fn, args, name) => { const [pat, ...rest] = args; if (!['seq', 'cat', 'stack'].includes(name)) { args = [...rest, pat]; } - - // console.log('call', name, ...flipped); - return fn(...args); }; @@ -30,6 +27,12 @@ export function mondo(code, offset = 0) { if (Array.isArray(code)) { code = code.join(''); } - const pat = runner.run(code, { pipepost: true }); + const pat = runner.run(code, offset); return pat; } + +// tell transpiler how to get locations for mondo`` calls +registerLanguage('mondo', { + getLocations: (code, offset) => runner.parser.get_locations(code, offset), +}); + diff --git a/packages/mondough/package.json b/packages/mondough/package.json index 0754a697f..f444c5560 100644 --- a/packages/mondough/package.json +++ b/packages/mondough/package.json @@ -32,7 +32,8 @@ }, "homepage": "https://github.com/tidalcycles/strudel#readme", "dependencies": { - "@strudel/core": "workspace:*" + "@strudel/core": "workspace:*", + "@strudel/transpiler": "workspace:*" }, "devDependencies": { "mondo": "*", diff --git a/packages/transpiler/transpiler.mjs b/packages/transpiler/transpiler.mjs index 2e566305f..5eeecd62b 100644 --- a/packages/transpiler/transpiler.mjs +++ b/packages/transpiler/transpiler.mjs @@ -8,6 +8,16 @@ export function registerWidgetType(type) { widgetMethods.push(type); } +let languages = new Map(); +// config = { getLocations: (code: string, offset?: number) => number[][] } +// see mondough.mjs for example use +// the language will kick in when the code contains a template literal of type +// example: mondo`...` will use language of type "mondo" +// TODO: refactor tidal.mjs to use this +export function registerLanguage(type, config) { + languages.set(type, config); +} + export function transpiler(input, options = {}) { const { wrapAsync = false, addReturn = true, emitMiniLocations = true, emitWidgets = true } = options; @@ -26,7 +36,19 @@ export function transpiler(input, options = {}) { walk(ast, { enter(node, parent /* , prop, index */) { - if (isTidalTeplateLiteral(node)) { + if (isLanguageLiteral(node)) { + const { name } = node.tag; + const language = languages.get(name); + const code = node.quasi.quasis[0].value.raw; + const offset = node.quasi.start + 1; + if (emitMiniLocations) { + const locs = language.getLocations(code, offset); + miniLocations = miniLocations.concat(locs); + } + this.skip(); + return this.replace(languageWithLocation(name, code, offset)); + } + if (isTemplateLiteral(node, 'tidal')) { const raw = node.quasi.quasis[0].value.raw; const offset = node.quasi.start + 1; if (emitMiniLocations) { @@ -219,12 +241,16 @@ function labelToP(node) { }; } +function isLanguageLiteral(node) { + return node.type === 'TaggedTemplateExpression' && languages.has(node.tag.name); +} + // tidal highlighting // this feels kind of stupid, when we also know the location inside the string op (tidal.mjs) // but maybe it's the only way -function isTidalTeplateLiteral(node) { - return node.type === 'TaggedTemplateExpression' && node.tag.name === 'tidal'; +function isTemplateLiteral(node, value) { + return node.type === 'TaggedTemplateExpression' && node.tag.name === value; } function collectHaskellMiniLocations(haskellCode, offset) { @@ -262,3 +288,33 @@ function tidalWithLocation(value, offset) { optional: false, }; } + +function mondoWithLocation(value, offset) { + return { + type: 'CallExpression', + callee: { + type: 'Identifier', + name: 'mondo', + }, + arguments: [ + { type: 'Literal', value }, + { type: 'Literal', value: offset }, + ], + optional: false, + }; +} + +function languageWithLocation(name, value, offset) { + return { + type: 'CallExpression', + callee: { + type: 'Identifier', + name: name, + }, + arguments: [ + { type: 'Literal', value }, + { type: 'Literal', value: offset }, + ], + optional: false, + }; +} From 95526ac99c7db610b7246e4f48f75895f490c63b Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 16 Mar 2025 21:04:28 +0100 Subject: [PATCH 014/174] fix: formatting --- packages/mondough/mondough.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index 03437be64..2388a9353 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -35,4 +35,3 @@ export function mondo(code, offset = 0) { registerLanguage('mondo', { getLocations: (code, offset) => runner.parser.get_locations(code, offset), }); - From ea61627963e28ac15714fcba1d278fffb1010a4b Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 16 Mar 2025 21:05:04 +0100 Subject: [PATCH 015/174] fix: lint error --- packages/mondo/mondo.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index be5e819fe..da8c31ecd 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -19,7 +19,7 @@ export class MondoParser { pipe: /^\./, stack: /^,/, op: /^[*/]/, - plain: /^[a-zA-Z0-9-_\^]+/, + plain: /^[a-zA-Z0-9-_^]+/, }; // matches next token next_token(code, offset = 0) { From 902012759ae16d9cd4ff29243751c767b63724d9 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 16 Mar 2025 21:24:22 +0100 Subject: [PATCH 016/174] fix:support negative numbers --- packages/mondo/mondo.mjs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index da8c31ecd..5ca3a0f81 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -15,7 +15,7 @@ export class MondoParser { close_cat: /^>/, open_seq: /^\[/, close_seq: /^\]/, - number: /^[0-9]*\.?[0-9]+/, // before pipe! + number: /^-?[0-9]*\.?[0-9]+/, // before pipe! pipe: /^\./, stack: /^,/, op: /^[*/]/, @@ -299,6 +299,13 @@ export class MondoRunner { // console.log(printAst(ast)); return this.call(ast); } + // todo: always use lib.call? + libcall(fn, args, name) { + if (this.lib.call) { + return this.lib.call(fn, args, name); + } + return fn(...args); + } call(ast) { // for a node to be callable, it needs to be a list this.assert(ast.type === 'list', `function call: expected list, got ${ast.type}`); @@ -325,15 +332,12 @@ export class MondoRunner { const callee = ast.children[1].value; const innerFn = this.lib[callee]; this.assert(innerFn, `function call: unknown function name "${callee}"`); - return (pat) => innerFn(pat, args.slice(1)); + return (pat) => this.libcall(innerFn, [pat, ...args.slice(1)], callee); } // look up function in lib const fn = this.lib[name]; this.assert(fn, `function call: unknown function name "${name}"`); - if (this.lib.call) { - return this.lib.call(fn, args, name); - } - return fn(...args); + return this.libcall(fn, args, name); } } From 77ade0758e95c28383bde99b145fdf6d399688db Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 16 Mar 2025 22:07:03 +0100 Subject: [PATCH 017/174] mondo: slightly improve error handling --- packages/mondo/mondo.mjs | 20 ++++++++++++++------ packages/mondough/mondough.mjs | 1 + 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 5ca3a0f81..829f34278 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -306,11 +306,19 @@ export class MondoRunner { } return fn(...args); } + errorhead(ast) { + return `[mondo ${ast.loc?.join(':') || ''}]`; + } call(ast) { // for a node to be callable, it needs to be a list - this.assert(ast.type === 'list', `function call: expected list, got ${ast.type}`); + this.assert(ast.type === 'list', `${this.errorhead(ast)} function call: expected list, got ${ast.type}`); // the first element is expected to be the function name - this.assert(ast.children[0]?.type === 'plain', `function call: expected first child to be plain, got ${ast.type}`); + const first = ast.children[0]; + const name = first.value; + this.assert( + first?.type === 'plain', + `${this.errorhead(first)} expected first child to be function name, got ${first.type}${name ? ` "${name}"` : ''}.`, + ); // process args const args = ast.children.slice(1).map((arg) => { @@ -326,18 +334,18 @@ export class MondoRunner { return this.call(arg); }); - const name = ast.children[0].value; if (name === '.') { // lambda : (.fast 2) = x=>fast(2, x) - const callee = ast.children[1].value; + const second = ast.children[1]; + const callee = second.value; const innerFn = this.lib[callee]; - this.assert(innerFn, `function call: unknown function name "${callee}"`); + this.assert(innerFn, `${this.errorhead(second)} lambda error: unknown function name "${callee}"`); return (pat) => this.libcall(innerFn, [pat, ...args.slice(1)], callee); } // look up function in lib const fn = this.lib[name]; - this.assert(fn, `function call: unknown function name "${name}"`); + this.assert(fn, `${this.errorhead(first)} function call: unknown function name "${name}"`); return this.libcall(fn, args, name); } } diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index 2388a9353..87089de73 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -11,6 +11,7 @@ let getLeaf = (value, token) => { strudelScope.plain = getLeaf; strudelScope.number = getLeaf; +strudelScope.string = getLeaf; strudelScope.call = (fn, args, name) => { const [pat, ...rest] = args; From 9b8761bc454ac36767e2684542c2dce2aa922f4d Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 16 Mar 2025 22:31:40 +0100 Subject: [PATCH 018/174] fix: top-level functions arp/arpWith --- packages/core/pattern.mjs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index c1e95211d..ebd5115cf 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -904,12 +904,13 @@ Pattern.prototype.collect = function () { * note("<[c,eb,g]!2 [c,f,ab] [d,f,ab]>") * .arpWith(haps => haps[2]) * */ -Pattern.prototype.arpWith = function (func) { - return this.collect() +export const arpWith = register('arpWith', (func, pat) => { + return pat + .collect() .fmap((v) => reify(func(v))) .innerJoin() .withHap((h) => new Hap(h.whole, h.part, h.value.value, h.combineContext(h.value))); -}; +}); /** * Selects indices in in stacked notes. @@ -917,9 +918,11 @@ Pattern.prototype.arpWith = function (func) { * note("<[c,eb,g]!2 [c,f,ab] [d,f,ab]>") * .arp("0 [0,2] 1 [0,2]") * */ -Pattern.prototype.arp = function (pat) { - return this.arpWith((haps) => pat.fmap((i) => haps[i % haps.length])); -}; +export const arp = register( + 'arp', + (indices, pat) => pat.arpWith((haps) => reify(indices).fmap((i) => haps[i % haps.length])), + false, +); /* * Takes a time duration followed by one or more patterns, and shifts the given patterns in time, so they are From 3c3832dbcecf3c94d81b4bb1a408c95f547741ae Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 16 Mar 2025 23:08:31 +0100 Subject: [PATCH 019/174] superdough: native support for rest symbols - ~ in s --- packages/superdough/superdough.mjs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index fc81f565e..729f4316d 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -481,6 +481,9 @@ export const superdough = async (value, t, hapDuration) => { const onended = () => { toDisconnect.forEach((n) => n?.disconnect()); }; + if (['-', '~'].includes(s)) { + return; + } if (bank && s) { s = `${bank}_${s}`; value.s = s; From 2dd445c1024c22d071d49649fa7b22d0d312dbea Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 16 Mar 2025 23:10:23 +0100 Subject: [PATCH 020/174] mondo: support variables --- packages/mondough/mondough.mjs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index 87089de73..002a7ae5a 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -6,6 +6,9 @@ let runner = new MondoRunner(strudelScope, { pipepost: true, loc: true }); let getLeaf = (value, token) => { const [from, to] = token.loc; + if (strudelScope[value]) { + return strudelScope[value].withLoc(from, to); + } return reify(value).withLoc(from, to); }; From 118b619b740535bb06aff9a9b0df54b57572a17c Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 16 Mar 2025 23:10:52 +0100 Subject: [PATCH 021/174] mondo: support - ~ in plain type + simplify errors --- packages/mondo/mondo.mjs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 829f34278..cb088fa9f 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -19,7 +19,7 @@ export class MondoParser { pipe: /^\./, stack: /^,/, op: /^[*/]/, - plain: /^[a-zA-Z0-9-_^]+/, + plain: /^[a-zA-Z0-9-~_^]+/, }; // matches next token next_token(code, offset = 0) { @@ -317,7 +317,7 @@ export class MondoRunner { const name = first.value; this.assert( first?.type === 'plain', - `${this.errorhead(first)} expected first child to be function name, got ${first.type}${name ? ` "${name}"` : ''}.`, + `${this.errorhead(first)} expected function name, got ${first.type}${name ? ` "${name}"` : ''}.`, ); // process args @@ -339,13 +339,13 @@ export class MondoRunner { const second = ast.children[1]; const callee = second.value; const innerFn = this.lib[callee]; - this.assert(innerFn, `${this.errorhead(second)} lambda error: unknown function name "${callee}"`); + this.assert(innerFn, `${this.errorhead(second)} unknown function name "${callee}"`); return (pat) => this.libcall(innerFn, [pat, ...args.slice(1)], callee); } // look up function in lib const fn = this.lib[name]; - this.assert(fn, `${this.errorhead(first)} function call: unknown function name "${name}"`); + this.assert(fn, `${this.errorhead(first)} unknown function name "${name}"`); return this.libcall(fn, args, name); } } From 40f64891118acf33f3dabe965c336c12bfb8794e Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 16 Mar 2025 23:30:41 +0100 Subject: [PATCH 022/174] reify variables --- packages/mondough/mondough.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index 002a7ae5a..21250ad20 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -1,4 +1,4 @@ -import { strudelScope, reify, fast, slow } from '@strudel/core'; +import { strudelScope, reify, fast, slow, isPattern } from '@strudel/core'; import { registerLanguage } from '@strudel/transpiler'; import { MondoRunner } from '../mondo/mondo.mjs'; @@ -7,7 +7,7 @@ let runner = new MondoRunner(strudelScope, { pipepost: true, loc: true }); let getLeaf = (value, token) => { const [from, to] = token.loc; if (strudelScope[value]) { - return strudelScope[value].withLoc(from, to); + return reify(strudelScope[value]).withLoc(from, to); } return reify(value).withLoc(from, to); }; From 7db52b3dc26105e7f79db0eeabebab7926f8d318 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Tue, 18 Mar 2025 22:33:14 +0100 Subject: [PATCH 023/174] mondo improvements: - add second string type - add $ as alias for stack - simplify leaf handling --- packages/mondo/mondo.mjs | 38 +++++++++++++++++------------- packages/mondo/test/mondo.test.mjs | 2 +- packages/mondough/mondough.mjs | 15 ++++++------ 3 files changed, 30 insertions(+), 25 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index cb088fa9f..68ce390d4 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -8,7 +8,8 @@ This program is free software: you can redistribute it and/or modify it under th export class MondoParser { // these are the tokens we expect token_types = { - string: /^"(.*?)"/, + quotes_double: /^"(.*?)"/, + quotes_single: /^'(.*?)'/, open_list: /^\(/, close_list: /^\)/, open_cat: /^ 1 || expressions[0].type !== 'list') { return { type: 'list', - children: this.desugar_children(expressions), + children: this.desugar(expressions), }; } // we have a single list @@ -110,7 +111,7 @@ export class MondoParser { let commaIndex = children.findIndex((child) => child.type === split_type); if (commaIndex === -1) break; const chunk = children.slice(0, commaIndex); - chunks.push(chunk); + chunk.length && chunks.push(chunk); children = children.slice(commaIndex + 1); } if (!chunks.length) { @@ -227,24 +228,26 @@ export class MondoParser { this.consume(close_type); return children; } + desugar(children, type) { + // not really needed but more readable and might be extended in the future + children = this.desugar_stack(children, type); + return children; + } parse_list() { let children = this.parse_pair('open_list', 'close_list'); - children = this.desugar_stack(children); - children = this.desugar_children(children); + children = this.desugar(children); return { type: 'list', children }; } parse_cat() { let children = this.parse_pair('open_cat', 'close_cat'); children = [{ type: 'plain', value: 'cat' }, ...children]; - children = this.desugar_stack(children, 'cat'); - children = this.desugar_children(children); + children = this.desugar(children, 'cat'); return { type: 'list', children }; } parse_seq() { let children = this.parse_pair('open_seq', 'close_seq'); children = [{ type: 'plain', value: 'seq' }, ...children]; - children = this.desugar_stack(children, 'seq'); - children = this.desugar_children(children); + children = this.desugar(children, 'seq'); return { type: 'list', children }; } consume(type) { @@ -322,16 +325,19 @@ export class MondoRunner { // process args const args = ast.children.slice(1).map((arg) => { - if (arg.type === 'string') { - return this.lib.string(arg.value.slice(1, -1), arg); + if (arg.type === 'list') { + return this.call(arg); } - if (arg.type === 'plain') { - return this.lib.plain(arg.value, arg); + if (!this.lib.leaf) { + throw new Error(`no handler for leaft nodes! add leaf to your lib`); } + if (arg.type === 'number') { - return this.lib.number(Number(arg.value), arg); + arg.value = Number(arg.value); + } else if (['quotes_double', 'quotes_single'].includes(arg.type)) { + arg.value = arg.value.slice(1, -1); } - return this.call(arg); + return this.lib.leaf(arg); }); if (name === '.') { diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index 40595004d..a88041821 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th */ import { describe, expect, it } from 'vitest'; -import { MondoParser, MondoRunner, printAst } from '../mondo.mjs'; +import { MondoParser, printAst } from '../mondo.mjs'; const parser = new MondoParser(); const p = (code) => parser.parse(code, -1); diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index 21250ad20..52b169cae 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -1,21 +1,20 @@ -import { strudelScope, reify, fast, slow, isPattern } from '@strudel/core'; +import { strudelScope, reify, fast, slow } from '@strudel/core'; import { registerLanguage } from '@strudel/transpiler'; import { MondoRunner } from '../mondo/mondo.mjs'; -let runner = new MondoRunner(strudelScope, { pipepost: true, loc: true }); +let runner = new MondoRunner(strudelScope); -let getLeaf = (value, token) => { +strudelScope.leaf = (token) => { + let { value } = token; const [from, to] = token.loc; - if (strudelScope[value]) { + if (token.type === 'plain' && strudelScope[value]) { + // what if we want a string that happens to also be a variable name? + // example: "s sine" -> sine is also a variable return reify(strudelScope[value]).withLoc(from, to); } return reify(value).withLoc(from, to); }; -strudelScope.plain = getLeaf; -strudelScope.number = getLeaf; -strudelScope.string = getLeaf; - strudelScope.call = (fn, args, name) => { const [pat, ...rest] = args; if (!['seq', 'cat', 'stack'].includes(name)) { From 129fd7d6b0b7639c7e504d2e04e6daac61803c72 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 19 Mar 2025 00:13:06 +0100 Subject: [PATCH 024/174] add notes --- packages/mondo/README.md | 135 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) diff --git a/packages/mondo/README.md b/packages/mondo/README.md index d7ee20ac4..a6a59dd94 100644 --- a/packages/mondo/README.md +++ b/packages/mondo/README.md @@ -27,3 +27,138 @@ the above code will create the following call structure: ``` you can pass all available functions to *MondoRunner* as an object. + +## snippets / thoughts + +### variants of add + +```plaintext +n[0 1 2].add(<0 -4>.n).scale"C minor" +``` + +```js +n("0 1 2").add("<0 -4>".n()).scale("C:minor") +``` + +--- + +```plaintext +n[0 1 2].add(n<0 -4>).scale"C minor" +``` + +```js +n("0 1 2").add(n("<0 -4>")).scale("C:minor") +``` + +--- + +```plaintext +n[0 1 2].(add<0 -4>).scale"C minor" +``` + +```js +n("0 1 2".add("<0 -4>")).scale("C:minor") +``` + +--- + +```plaintext +n[0 1 2].scale"C minor" +.sometimes (12.note.add) +``` + +```js +n("0 1 2").scale("C:minor") +.sometimes(add(note("12"))) +``` + +--- + +```plaintext +note g2*8.dec /2.(range .1 .4) +``` + +```js +note("g2*8").dec(cat(sine, saw).slow(2).range(.1, .4)) +``` + +--- + +```plaintext +n <0 1 2 3 4>*4 .scale"C minor" .jux +``` + +```js +n("<0 1 2 3 4>*4").scale("C:minor").jux(cat(rev,press)) +``` + +--- + +mondo` +sound [bd sd.(every 3 (.fast 4))].jux +` +// og "Alternate Timelines for TidalCycles" example: +// jux <(rev) (iter 4)> $ sound [bd (every 3 (fast 4) [sn])] + +### things mondo cant do + +how to write lists? + +```js +arrange( + [4, "(3,8)"], + [2, "(5,8)"] +).note() +``` + +how to write objects: + +```js +samples({ rave: 'rave/AREUREADY.wav' }, 'github:tidalcycles/dirt-samples') +``` + +how to access array indices? + +```js +note("<[c,eb,g]!2 [c,f,ab] [d,f,ab]>").arpWith(haps => haps[2]) +``` + +s hh .struct(binaryN 55532 16) + +comments + +variables: note g2*8.dec sine + +sine.(range 0 4)/2 doesnt work +sine/2.(range 0 4) works + +n (irand 8. ribbon 0 2) .scale"C minor" => lags because no whole + +### reference + +- arp: note <[c,eb,g] [c,f,ab] [d,f,ab]> .arp [0 [0,2] 1 [0,2]] +- bank: s [bd sd [- bd] sd].bank TR909 +- beat: s sd .beat [4,12] 16 +- binary: s hh .struct (binary 5) +- binaryN: s hh .struct(binaryN 55532 16) => is wrong +- bite: n[0 1 2 3 4 5 6 7].scale"c mixolydian".bite 4 [3 2 1 0] +- bpattack: note [c2 e2 f2 g2].s sawtooth.bpf 500.bpa <.5 .25 .1 .01>/4.bpenv 4 + +### dot is a bit ambiguous + +```plaintext +n[0 1 2].scale"C minor".ad.1 +``` + +decimal vs pipe + +### less ambiguity with [] and "" + +in js, s("hh cp") implcitily does [hh cp] +in mondo, s[hh cp] always shows the type of bracket used + +### todo + +- lists: C:minor +- spread: [0 .. 2] +- replicate: ! From 88ca54461ee69783073a363e59604206547a3cb8 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 19 Mar 2025 00:15:53 +0100 Subject: [PATCH 025/174] rename @strudel/mondough to @strudel/mondo --- packages/mondough/package.json | 2 +- pnpm-lock.yaml | 5 ++++- website/package.json | 2 +- website/src/repl/util.mjs | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/mondough/package.json b/packages/mondough/package.json index f444c5560..eae22519e 100644 --- a/packages/mondough/package.json +++ b/packages/mondough/package.json @@ -1,5 +1,5 @@ { - "name": "@strudel/mondough", + "name": "@strudel/mondo", "version": "1.1.0", "description": "mondo notation for strudel", "main": "mondough.mjs", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f5b63c56f..7666d3fc2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -358,6 +358,9 @@ importers: '@strudel/core': specifier: workspace:* version: link:../core + '@strudel/transpiler': + specifier: workspace:* + version: link:../transpiler devDependencies: mondo: specifier: '*' @@ -690,7 +693,7 @@ importers: '@strudel/mini': specifier: workspace:* version: link:../packages/mini - '@strudel/mondough': + '@strudel/mondo': specifier: workspace:* version: link:../packages/mondough '@strudel/motion': diff --git a/website/package.json b/website/package.json index e42493520..069a26c73 100644 --- a/website/package.json +++ b/website/package.json @@ -42,7 +42,7 @@ "@strudel/tonal": "workspace:*", "@strudel/transpiler": "workspace:*", "@strudel/webaudio": "workspace:*", - "@strudel/mondough": "workspace:*", + "@strudel/mondo": "workspace:*", "@strudel/xen": "workspace:*", "@supabase/supabase-js": "^2.48.1", "@tailwindcss/forms": "^0.5.10", diff --git a/website/src/repl/util.mjs b/website/src/repl/util.mjs index cb5e6f36e..fb9df4b90 100644 --- a/website/src/repl/util.mjs +++ b/website/src/repl/util.mjs @@ -84,7 +84,7 @@ export function loadModules() { import('@strudel/gamepad'), import('@strudel/motion'), import('@strudel/mqtt'), - import('@strudel/mondough'), + import('@strudel/mondo'), ]; if (isTauri()) { modules = modules.concat([ From 642ddcdb59aa2df3202915936fab1f562734e9ce Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 19 Mar 2025 17:03:17 +0100 Subject: [PATCH 026/174] support : in mondo --- packages/mondo/mondo.mjs | 31 ++++++++++++++++++++++++++++++ packages/mondo/test/mondo.test.mjs | 2 ++ packages/mondough/mondough.mjs | 5 ++++- 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 68ce390d4..4402aa525 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -20,6 +20,7 @@ export class MondoParser { pipe: /^\./, stack: /^[,$]/, op: /^[*/]/, + tail: /^:/, plain: /^[a-zA-Z0-9-~_^]+/, }; // matches next token @@ -96,6 +97,7 @@ export class MondoParser { return this.consume(next); } desugar_children(children) { + children = this.resolve_tails(children); children = this.resolve_ops(children); children = this.resolve_pipes(children); return children; @@ -144,6 +146,35 @@ export class MondoParser { }); return [{ type: 'plain', value: 'stack' }, ...args]; } + resolve_tails(children) { + while (true) { + let opIndex = children.findIndex((child) => child.type === 'tail'); + if (opIndex === -1) break; + const op = { type: 'plain', value: children[opIndex].value }; + if (opIndex === children.length - 1) { + throw new Error(`cannot use operator as last child.`); + } + if (opIndex === 0) { + // regular function call (assuming each operator exists as function) + children[opIndex] = op; + continue; + } + const left = children[opIndex - 1]; + const right = children[opIndex + 1]; + + // convert infix to prefix notation + const call = { type: 'list', children: [op, left, right] }; + + // insert call while keeping other siblings + children = [...children.slice(0, opIndex - 1), call, ...children.slice(opIndex + 2)]; + // unwrap double list.. e.g. (s jazz) * 2 + if (children.length === 1) { + // there might be a cleaner solution + children = children[0].children; + } + } + return children; + } resolve_ops(children) { while (true) { let opIndex = children.findIndex((child) => child.type === 'op'); diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index a88041821..c78712e9d 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -99,6 +99,8 @@ describe('mondo sugar', () => { it('should desugar , ()', () => expect(desguar('(s bd, s cp)')).toEqual('(stack (s bd) (s cp))')); it('should desugar * /', () => expect(desguar('[a b*2 c d/3 e]')).toEqual('(seq a (* b 2) c (/ d 3) e)')); it('should desugar []*x', () => expect(desguar('[a [b c]*3]')).toEqual('(seq a (* (seq b c) 3))')); + it('should desugar x:y', () => expect(desguar('x:y')).toEqual('(: x y)')); + it('should desugar x:y:z', () => expect(desguar('x:y:z')).toEqual('(: (: x y) z)')); it('should desugar README example', () => expect(desguar('s [bd hh*2 cp.(crush 4) ] . speed .8')).toEqual( diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index 52b169cae..c7fa4bcef 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -17,7 +17,7 @@ strudelScope.leaf = (token) => { strudelScope.call = (fn, args, name) => { const [pat, ...rest] = args; - if (!['seq', 'cat', 'stack'].includes(name)) { + if (!['seq', 'cat', 'stack', ':'].includes(name)) { args = [...rest, pat]; } return fn(...args); @@ -26,6 +26,9 @@ strudelScope.call = (fn, args, name) => { strudelScope['*'] = fast; strudelScope['/'] = slow; +const tail = (pat, friend) => pat.fmap((a) => (b) => (Array.isArray(a) ? [...a, b] : [a, b])).appLeft(friend); +strudelScope[':'] = tail; + export function mondo(code, offset = 0) { if (Array.isArray(code)) { code = code.join(''); From e04f250adbd1f4fa89b370dcdfb29af47651a8a8 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 19 Mar 2025 18:16:37 +0100 Subject: [PATCH 027/174] mondo: proper lambda functions with local scope --- packages/mondo/mondo.mjs | 42 +++++++++++++++++++--------------- packages/mondough/mondough.mjs | 6 ++++- 2 files changed, 29 insertions(+), 19 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 4402aa525..fc4069434 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -214,11 +214,14 @@ export class MondoParser { if (pipeIndex === -1) break; // pipe up front => lambda if (pipeIndex === 0) { - // . as lambda: (.fast 2) = x=>x.fast(2) - // TODO: this doesn't work for (.fast 2 .speed 2) - // probably needs proper ast representation of lambda - children[pipeIndex] = { type: 'plain', value: '.' }; - continue; + // . as lambda: (.fast 2) = (lambda (_) (fast _ 2)) + const args = [{ type: 'plain', value: '_' }]; + const body = this.desugar([args[0], ...children]); + return [ + { type: 'plain', value: 'lambda' }, + { type: 'list', children: args }, + { type: 'list', children: body }, + ]; } const rightSide = children.slice(pipeIndex + 2); const right = children[pipeIndex + 1]; @@ -343,7 +346,7 @@ export class MondoRunner { errorhead(ast) { return `[mondo ${ast.loc?.join(':') || ''}]`; } - call(ast) { + call(ast, scope = []) { // for a node to be callable, it needs to be a list this.assert(ast.type === 'list', `${this.errorhead(ast)} function call: expected list, got ${ast.type}`); // the first element is expected to be the function name @@ -354,10 +357,22 @@ export class MondoRunner { `${this.errorhead(first)} expected function name, got ${first.type}${name ? ` "${name}"` : ''}.`, ); + if (name === 'lambda') { + const [_, args, body] = ast.children; + const argNames = args.children.map((child) => child.value); + // console.log('lambda', argNames, body.children); + return (x) => { + scope = { + [argNames[0]]: x, // TODO: merge scope... + support multiple args + }; + return this.call(body, scope); + }; + } + // process args const args = ast.children.slice(1).map((arg) => { if (arg.type === 'list') { - return this.call(arg); + return this.call(arg, scope); } if (!this.lib.leaf) { throw new Error(`no handler for leaft nodes! add leaf to your lib`); @@ -368,21 +383,12 @@ export class MondoRunner { } else if (['quotes_double', 'quotes_single'].includes(arg.type)) { arg.value = arg.value.slice(1, -1); } - return this.lib.leaf(arg); + return this.lib.leaf(arg, scope); }); - if (name === '.') { - // lambda : (.fast 2) = x=>fast(2, x) - const second = ast.children[1]; - const callee = second.value; - const innerFn = this.lib[callee]; - this.assert(innerFn, `${this.errorhead(second)} unknown function name "${callee}"`); - return (pat) => this.libcall(innerFn, [pat, ...args.slice(1)], callee); - } - // look up function in lib const fn = this.lib[name]; this.assert(fn, `${this.errorhead(first)} unknown function name "${name}"`); - return this.libcall(fn, args, name); + return this.libcall(fn, args, name, scope); } } diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index c7fa4bcef..868198ca0 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -4,8 +4,12 @@ import { MondoRunner } from '../mondo/mondo.mjs'; let runner = new MondoRunner(strudelScope); -strudelScope.leaf = (token) => { +strudelScope.leaf = (token, scope) => { let { value } = token; + // local scope + if (token.type === 'plain' && scope[value]) { + return reify(scope[value]); // -> local scope has no location + } const [from, to] = token.loc; if (token.type === 'plain' && strudelScope[value]) { // what if we want a string that happens to also be a variable name? From 208706fb52436b99341c384e5a5ee5068c6c3547 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 20 Mar 2025 09:09:29 +0100 Subject: [PATCH 028/174] simplify mondo: ":" is just another operator --- packages/mondo/mondo.mjs | 42 +++++------------------------- packages/mondo/test/mondo.test.mjs | 1 + 2 files changed, 8 insertions(+), 35 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index fc4069434..5997feb40 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -19,8 +19,7 @@ export class MondoParser { number: /^-?[0-9]*\.?[0-9]+/, // before pipe! pipe: /^\./, stack: /^[,$]/, - op: /^[*/]/, - tail: /^:/, + op: /^[*/:]/, plain: /^[a-zA-Z0-9-~_^]+/, }; // matches next token @@ -97,7 +96,6 @@ export class MondoParser { return this.consume(next); } desugar_children(children) { - children = this.resolve_tails(children); children = this.resolve_ops(children); children = this.resolve_pipes(children); return children; @@ -146,32 +144,10 @@ export class MondoParser { }); return [{ type: 'plain', value: 'stack' }, ...args]; } - resolve_tails(children) { - while (true) { - let opIndex = children.findIndex((child) => child.type === 'tail'); - if (opIndex === -1) break; - const op = { type: 'plain', value: children[opIndex].value }; - if (opIndex === children.length - 1) { - throw new Error(`cannot use operator as last child.`); - } - if (opIndex === 0) { - // regular function call (assuming each operator exists as function) - children[opIndex] = op; - continue; - } - const left = children[opIndex - 1]; - const right = children[opIndex + 1]; - - // convert infix to prefix notation - const call = { type: 'list', children: [op, left, right] }; - - // insert call while keeping other siblings - children = [...children.slice(0, opIndex - 1), call, ...children.slice(opIndex + 2)]; - // unwrap double list.. e.g. (s jazz) * 2 - if (children.length === 1) { - // there might be a cleaner solution - children = children[0].children; - } + // prevents to get a list, e.g. ((x y)) => (x y) + unwrap_children(children) { + if (children.length === 1) { + return children[0].children; } return children; } @@ -188,6 +164,7 @@ export class MondoParser { children[opIndex] = op; continue; } + // convert infix to prefix notation const left = children[opIndex - 1]; const right = children[opIndex + 1]; if (left.type === 'pipe') { @@ -195,15 +172,10 @@ export class MondoParser { children[opIndex] = op; continue; } - // convert infix to prefix notation const call = { type: 'list', children: [op, left, right] }; // insert call while keeping other siblings children = [...children.slice(0, opIndex - 1), call, ...children.slice(opIndex + 2)]; - // unwrap double list.. e.g. (s jazz) * 2 - if (children.length === 1) { - // there might be a cleaner solution - children = children[0].children; - } + children = this.unwrap_children(children); } return children; } diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index c78712e9d..e21ec75b0 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -101,6 +101,7 @@ describe('mondo sugar', () => { it('should desugar []*x', () => expect(desguar('[a [b c]*3]')).toEqual('(seq a (* (seq b c) 3))')); it('should desugar x:y', () => expect(desguar('x:y')).toEqual('(: x y)')); it('should desugar x:y:z', () => expect(desguar('x:y:z')).toEqual('(: (: x y) z)')); + it('should desugar x:y*x', () => expect(desguar('bd:0*2')).toEqual('(* (: bd 0) 2)')); it('should desugar README example', () => expect(desguar('s [bd hh*2 cp.(crush 4) ] . speed .8')).toEqual( From 65a7b3030e674c4bbd5e588fb93c81c5e178449a Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 20 Mar 2025 09:10:12 +0100 Subject: [PATCH 029/174] fix: markcss can now override styles (like color) --- packages/codemirror/highlight.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/codemirror/highlight.mjs b/packages/codemirror/highlight.mjs index d333986cc..f9f977c6b 100644 --- a/packages/codemirror/highlight.mjs +++ b/packages/codemirror/highlight.mjs @@ -1,4 +1,4 @@ -import { RangeSetBuilder, StateEffect, StateField } from '@codemirror/state'; +import { RangeSetBuilder, StateEffect, StateField, Prec } from '@codemirror/state'; import { Decoration, EditorView } from '@codemirror/view'; export const setMiniLocations = StateEffect.define(); @@ -134,5 +134,5 @@ export const isPatternHighlightingEnabled = (on, config) => { setTimeout(() => { updateMiniLocations(config.editor, config.miniLocations); }, 100); - return on ? highlightExtension : []; + return on ? Prec.highest(highlightExtension) : []; }; From 3657e2fd65e11af40bbe9f2a3b66f67e8423e871 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 20 Mar 2025 09:10:45 +0100 Subject: [PATCH 030/174] mondo: change default markcss --- packages/mondough/mondough.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index 868198ca0..33e0116df 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -38,7 +38,7 @@ export function mondo(code, offset = 0) { code = code.join(''); } const pat = runner.run(code, offset); - return pat; + return pat.markcss('color: var(--foreground);text-decoration:underline'); } // tell transpiler how to get locations for mondo`` calls From 55a5f1d62a187493da3dfda9f0a22a0c4ab604b7 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 20 Mar 2025 09:10:54 +0100 Subject: [PATCH 031/174] transpiler cleanup --- packages/transpiler/transpiler.mjs | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/packages/transpiler/transpiler.mjs b/packages/transpiler/transpiler.mjs index 5eeecd62b..0ee371e6a 100644 --- a/packages/transpiler/transpiler.mjs +++ b/packages/transpiler/transpiler.mjs @@ -289,21 +289,6 @@ function tidalWithLocation(value, offset) { }; } -function mondoWithLocation(value, offset) { - return { - type: 'CallExpression', - callee: { - type: 'Identifier', - name: 'mondo', - }, - arguments: [ - { type: 'Literal', value }, - { type: 'Literal', value: offset }, - ], - optional: false, - }; -} - function languageWithLocation(name, value, offset) { return { type: 'CallExpression', From 3505732afad89870286d2c0083887375c62e6b44 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 20 Mar 2025 09:34:47 +0100 Subject: [PATCH 032/174] mondo: add .. operator --- packages/mondo/mondo.mjs | 2 +- packages/mondo/test/mondo.test.mjs | 1 + packages/mondough/mondough.mjs | 11 ++++++++++- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 5997feb40..70c58aa6d 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -17,9 +17,9 @@ export class MondoParser { open_seq: /^\[/, close_seq: /^\]/, number: /^-?[0-9]*\.?[0-9]+/, // before pipe! + op: /^[*\/:]|^\.{2}/, // * / : .. pipe: /^\./, stack: /^[,$]/, - op: /^[*/:]/, plain: /^[a-zA-Z0-9-~_^]+/, }; // matches next token diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index e21ec75b0..aadbafbeb 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -102,6 +102,7 @@ describe('mondo sugar', () => { it('should desugar x:y', () => expect(desguar('x:y')).toEqual('(: x y)')); it('should desugar x:y:z', () => expect(desguar('x:y:z')).toEqual('(: (: x y) z)')); it('should desugar x:y*x', () => expect(desguar('bd:0*2')).toEqual('(* (: bd 0) 2)')); + it('should desugar a..b', () => expect(desguar('0..2')).toEqual('(.. 0 2)')); it('should desugar README example', () => expect(desguar('s [bd hh*2 cp.(crush 4) ] . speed .8')).toEqual( diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index 33e0116df..e69ea0994 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -21,7 +21,7 @@ strudelScope.leaf = (token, scope) => { strudelScope.call = (fn, args, name) => { const [pat, ...rest] = args; - if (!['seq', 'cat', 'stack', ':'].includes(name)) { + if (!['seq', 'cat', 'stack', ':', '..'].includes(name)) { args = [...rest, pat]; } return fn(...args); @@ -30,9 +30,18 @@ strudelScope.call = (fn, args, name) => { strudelScope['*'] = fast; strudelScope['/'] = slow; +// : operator const tail = (pat, friend) => pat.fmap((a) => (b) => (Array.isArray(a) ? [...a, b] : [a, b])).appLeft(friend); strudelScope[':'] = tail; +// .. operator +const arrayRange = (start, stop, step = 1) => + Array.from({ length: Math.abs(stop - start) / step + 1 }, (_, index) => + start < stop ? start + index * step : start - index * step, + ); +const range = (min, max) => min.squeezeBind((a) => max.bind((b) => seq(...arrayRange(a, b)))); +strudelScope['..'] = range; + export function mondo(code, offset = 0) { if (Array.isArray(code)) { code = code.join(''); From d2e93a99f11a5ccf0475acd5888f9a84435b1421 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 20 Mar 2025 10:24:22 +0100 Subject: [PATCH 033/174] fix: lint --- packages/mondo/mondo.mjs | 2 +- packages/mondough/mondough.mjs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 70c58aa6d..ff0b6741e 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -17,7 +17,7 @@ export class MondoParser { open_seq: /^\[/, close_seq: /^\]/, number: /^-?[0-9]*\.?[0-9]+/, // before pipe! - op: /^[*\/:]|^\.{2}/, // * / : .. + op: /^[*/:]|^\.{2}/, // * / : .. pipe: /^\./, stack: /^[,$]/, plain: /^[a-zA-Z0-9-~_^]+/, diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index e69ea0994..857a9f000 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -1,4 +1,4 @@ -import { strudelScope, reify, fast, slow } from '@strudel/core'; +import { strudelScope, reify, fast, slow, seq } from '@strudel/core'; import { registerLanguage } from '@strudel/transpiler'; import { MondoRunner } from '../mondo/mondo.mjs'; From 432e8dcccc7806ee42f8ed059decb485969edefd Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 20 Mar 2025 13:15:48 +0100 Subject: [PATCH 034/174] mondo: support ! and @, also express seq and cat with stepcat --- packages/mondo/mondo.mjs | 2 +- packages/mondough/mondough.mjs | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index ff0b6741e..d7c90139f 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -17,7 +17,7 @@ export class MondoParser { open_seq: /^\[/, close_seq: /^\]/, number: /^-?[0-9]*\.?[0-9]+/, // before pipe! - op: /^[*/:]|^\.{2}/, // * / : .. + op: /^[*/:!@]|^\.{2}/, // * / : .. pipe: /^\./, stack: /^[,$]/, plain: /^[a-zA-Z0-9-~_^]+/, diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index 857a9f000..d442a0262 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -21,14 +21,22 @@ strudelScope.leaf = (token, scope) => { strudelScope.call = (fn, args, name) => { const [pat, ...rest] = args; - if (!['seq', 'cat', 'stack', ':', '..'].includes(name)) { + if (!['seq', 'cat', 'stack', ':', '..', '!', '@'].includes(name)) { args = [...rest, pat]; } + if (name === 'seq') { + return stepcat(...args).setSteps(1); + } + if (name === 'cat') { + return stepcat(...args).pace(1); + } return fn(...args); }; strudelScope['*'] = fast; strudelScope['/'] = slow; +strudelScope['!'] = (pat, n) => pat.extend(n); +strudelScope['@'] = (pat, n) => pat.expand(n); // : operator const tail = (pat, friend) => pat.fmap((a) => (b) => (Array.isArray(a) ? [...a, b] : [a, b])).appLeft(friend); From 9626f3cc9a58be793fd4be7cd9eacc6e0b38dadf Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 20 Mar 2025 13:24:31 +0100 Subject: [PATCH 035/174] mondo add % for pace --- packages/mondo/mondo.mjs | 2 +- packages/mondough/mondough.mjs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index d7c90139f..bcd48aafe 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -17,7 +17,7 @@ export class MondoParser { open_seq: /^\[/, close_seq: /^\]/, number: /^-?[0-9]*\.?[0-9]+/, // before pipe! - op: /^[*/:!@]|^\.{2}/, // * / : .. + op: /^[*/:!@%]|^\.{2}/, // * / : ! @ % .. pipe: /^\./, stack: /^[,$]/, plain: /^[a-zA-Z0-9-~_^]+/, diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index d442a0262..6de612324 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -21,7 +21,7 @@ strudelScope.leaf = (token, scope) => { strudelScope.call = (fn, args, name) => { const [pat, ...rest] = args; - if (!['seq', 'cat', 'stack', ':', '..', '!', '@'].includes(name)) { + if (!['seq', 'cat', 'stack', ':', '..', '!', '@', '%'].includes(name)) { args = [...rest, pat]; } if (name === 'seq') { @@ -37,6 +37,7 @@ strudelScope['*'] = fast; strudelScope['/'] = slow; strudelScope['!'] = (pat, n) => pat.extend(n); strudelScope['@'] = (pat, n) => pat.expand(n); +strudelScope['%'] = (pat, n) => pat.pace(n); // : operator const tail = (pat, friend) => pat.fmap((a) => (b) => (Array.isArray(a) ? [...a, b] : [a, b])).appLeft(friend); From e16295623d3a4861774791e2306d9398f676554f Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 20 Mar 2025 14:09:32 +0100 Subject: [PATCH 036/174] mondo: use stepcat for curly braces --- packages/mondo/mondo.mjs | 15 +++++++++++++-- packages/mondough/mondough.mjs | 13 +++++-------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index bcd48aafe..4ab8a793c 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -12,10 +12,12 @@ export class MondoParser { quotes_single: /^'(.*?)'/, open_list: /^\(/, close_list: /^\)/, - open_cat: /^/, - open_seq: /^\[/, + open_seq: /^\[/, // todo: rename square close_seq: /^\]/, + open_curly: /^\{/, + close_curly: /^\}/, number: /^-?[0-9]*\.?[0-9]+/, // before pipe! op: /^[*/:!@%]|^\.{2}/, // * / : ! @ % .. pipe: /^\./, @@ -93,6 +95,9 @@ export class MondoParser { if (next === 'open_seq') { return this.parse_seq(); } + if (next === 'open_curly') { + return this.parse_curly(); + } return this.consume(next); } desugar_children(children) { @@ -256,6 +261,12 @@ export class MondoParser { children = this.desugar(children, 'seq'); return { type: 'list', children }; } + parse_curly() { + let children = this.parse_pair('open_curly', 'close_curly'); + children = [{ type: 'plain', value: 'curly' }, ...children]; + children = this.desugar(children, 'curly'); + return { type: 'list', children }; + } consume(type) { // shift removes first element and returns it const token = this.tokens.shift(); diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index 6de612324..e81cfecb7 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -1,4 +1,4 @@ -import { strudelScope, reify, fast, slow, seq } from '@strudel/core'; +import { strudelScope, reify, fast, slow, seq, stepcat } from '@strudel/core'; import { registerLanguage } from '@strudel/transpiler'; import { MondoRunner } from '../mondo/mondo.mjs'; @@ -18,18 +18,15 @@ strudelScope.leaf = (token, scope) => { } return reify(value).withLoc(from, to); }; +strudelScope.curly = stepcat; +strudelScope.seq = (...args) => stepcat(...args).setSteps(1); +strudelScope.cat = (...args) => stepcat(...args).pace(1); strudelScope.call = (fn, args, name) => { const [pat, ...rest] = args; - if (!['seq', 'cat', 'stack', ':', '..', '!', '@', '%'].includes(name)) { + if (!['seq', 'cat', 'stack', 'curly', ':', '..', '!', '@', '%'].includes(name)) { args = [...rest, pat]; } - if (name === 'seq') { - return stepcat(...args).setSteps(1); - } - if (name === 'cat') { - return stepcat(...args).pace(1); - } return fn(...args); }; From c691916cbfe0f00cd0c0d99186a1d95d5eb0d2c7 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 20 Mar 2025 17:55:40 +0100 Subject: [PATCH 037/174] mondo: generic bracket names + simplify call handler + refactor mondough --- packages/mondo/mondo.mjs | 54 ++++++++----------- packages/mondo/test/mondo.test.mjs | 46 ++++++++-------- packages/mondough/mondough.mjs | 84 ++++++++++++++++-------------- 3 files changed, 90 insertions(+), 94 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 4ab8a793c..19ed27e95 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -12,10 +12,10 @@ export class MondoParser { quotes_single: /^'(.*?)'/, open_list: /^\(/, close_list: /^\)/, - open_cat: /^/, - open_seq: /^\[/, // todo: rename square - close_seq: /^\]/, + open_angle: /^/, + open_square: /^\[/, + close_square: /^\]/, open_curly: /^\{/, close_curly: /^\}/, number: /^-?[0-9]*\.?[0-9]+/, // before pipe! @@ -89,11 +89,11 @@ export class MondoParser { if (next === 'open_list') { return this.parse_list(); } - if (next === 'open_cat') { - return this.parse_cat(); + if (next === 'open_angle') { + return this.parse_angle(); } - if (next === 'open_seq') { - return this.parse_seq(); + if (next === 'open_square') { + return this.parse_square(); } if (next === 'open_curly') { return this.parse_curly(); @@ -126,7 +126,7 @@ export class MondoParser { return chunks; } desugar_stack(children, sequence_type) { - // children is expected to contain seq or cat as first item + // children is expected to contain square or angle as first item const chunks = this.split_children(children, 'stack', sequence_type); if (!chunks.length) { return this.desugar_children(children); @@ -140,7 +140,7 @@ export class MondoParser { // chunks of multiple args if (sequence_type) { // if given, each chunk needs to be prefixed - // [a b, c d] => (stack (seq a b) (seq c d)) + // [a b, c d] => (stack (square a b) (square c d)) chunk = [{ type: 'plain', value: sequence_type }, ...chunk]; } chunk = this.desugar_children(chunk); @@ -249,16 +249,16 @@ export class MondoParser { children = this.desugar(children); return { type: 'list', children }; } - parse_cat() { - let children = this.parse_pair('open_cat', 'close_cat'); - children = [{ type: 'plain', value: 'cat' }, ...children]; - children = this.desugar(children, 'cat'); + parse_angle() { + let children = this.parse_pair('open_angle', 'close_angle'); + children = [{ type: 'plain', value: 'angle' }, ...children]; + children = this.desugar(children, 'angle'); return { type: 'list', children }; } - parse_seq() { - let children = this.parse_pair('open_seq', 'close_seq'); - children = [{ type: 'plain', value: 'seq' }, ...children]; - children = this.desugar(children, 'seq'); + parse_square() { + let children = this.parse_pair('open_square', 'close_square'); + children = [{ type: 'plain', value: 'square' }, ...children]; + children = this.desugar(children, 'square'); return { type: 'list', children }; } parse_curly() { @@ -307,6 +307,8 @@ export class MondoRunner { constructor(lib) { this.parser = new MondoParser(); this.lib = lib; + this.assert(!!this.lib.leaf, `no handler for leaft nodes! add "leaf" to your lib`); + this.assert(!!this.lib.call, `no handler for call nodes! add "call" to your lib`); } // a helper to check conditions and throw if they are not met assert(condition, error) { @@ -316,16 +318,9 @@ export class MondoRunner { } run(code, offset = 0) { const ast = this.parser.parse(code, offset); - // console.log(printAst(ast)); + console.log(printAst(ast)); return this.call(ast); } - // todo: always use lib.call? - libcall(fn, args, name) { - if (this.lib.call) { - return this.lib.call(fn, args, name); - } - return fn(...args); - } errorhead(ast) { return `[mondo ${ast.loc?.join(':') || ''}]`; } @@ -357,9 +352,6 @@ export class MondoRunner { if (arg.type === 'list') { return this.call(arg, scope); } - if (!this.lib.leaf) { - throw new Error(`no handler for leaft nodes! add leaf to your lib`); - } if (arg.type === 'number') { arg.value = Number(arg.value); @@ -370,8 +362,6 @@ export class MondoRunner { }); // look up function in lib - const fn = this.lib[name]; - this.assert(fn, `${this.errorhead(first)} unknown function name "${name}"`); - return this.libcall(fn, args, name, scope); + return this.lib.call(name, args, scope); } } diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index aadbafbeb..1b9a41e3d 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -12,15 +12,15 @@ const p = (code) => parser.parse(code, -1); describe('mondo tokenizer', () => { const parser = new MondoParser(); - it('should tokenize with locations', () => + it('should tokenize with loangleions', () => expect( parser .tokenize('(one two three)') .map((t) => t.value + '=' + t.loc.join('-')) .join(' '), ).toEqual('(=0-1 one=1-4 two=5-8 three=9-14 )=14-15')); - // it('should parse with locations', () => expect(parser.parse('(one two three)')).toEqual()); - it('should get locations', () => + // it('should parse with loangleions', () => expect(parser.parse('(one two three)')).toEqual()); + it('should get loangleions', () => expect(parser.get_locations('s bd rim')).toEqual([ [2, 4], [5, 8], @@ -73,32 +73,34 @@ let desguar = (a) => { }; describe('mondo sugar', () => { - it('should desugar []', () => expect(desguar('[a b c]')).toEqual('(seq a b c)')); - it('should desugar [] nested', () => expect(desguar('[a [b c] d]')).toEqual('(seq a (seq b c) d)')); - it('should desugar <>', () => expect(desguar('')).toEqual('(cat a b c)')); - it('should desugar <> nested', () => expect(desguar(' d>')).toEqual('(cat a (cat b c) d)')); - it('should desugar mixed [] <>', () => expect(desguar('[a ]')).toEqual('(seq a (cat b c))')); - it('should desugar mixed <> []', () => expect(desguar('')).toEqual('(cat a (seq b c))')); + it('should desugar []', () => expect(desguar('[a b c]')).toEqual('(square a b c)')); + it('should desugar [] nested', () => expect(desguar('[a [b c] d]')).toEqual('(square a (square b c) d)')); + it('should desugar <>', () => expect(desguar('')).toEqual('(angle a b c)')); + it('should desugar <> nested', () => expect(desguar(' d>')).toEqual('(angle a (angle b c) d)')); + it('should desugar mixed [] <>', () => expect(desguar('[a ]')).toEqual('(square a (angle b c))')); + it('should desugar mixed <> []', () => expect(desguar('')).toEqual('(angle a (square b c))')); it('should desugar .', () => expect(desguar('s jazz . fast 2')).toEqual('(fast (s jazz) 2)')); - it('should desugar . seq', () => expect(desguar('[bd cp . fast 2]')).toEqual('(fast (seq bd cp) 2)')); + it('should desugar . square', () => expect(desguar('[bd cp . fast 2]')).toEqual('(fast (square bd cp) 2)')); it('should desugar . twice', () => expect(desguar('s jazz . fast 2 . slow 2')).toEqual('(slow (fast (s jazz) 2) 2)')); it('should desugar . nested', () => expect(desguar('(s cp . fast 2)')).toEqual('(fast (s cp) 2)')); - it('should desugar . within []', () => expect(desguar('[bd cp . fast 2]')).toEqual('(fast (seq bd cp) 2)')); + it('should desugar . within []', () => expect(desguar('[bd cp . fast 2]')).toEqual('(fast (square bd cp) 2)')); it('should desugar . within , within []', () => - expect(desguar('[bd cp . fast 2, x]')).toEqual('(stack (fast (seq bd cp) 2) x)')); + expect(desguar('[bd cp . fast 2, x]')).toEqual('(stack (fast (square bd cp) 2) x)')); - it('should desugar . ()', () => expect(desguar('[jazz hh.(fast 2)]')).toEqual('(seq jazz (fast hh 2))')); + it('should desugar . ()', () => expect(desguar('[jazz hh.(fast 2)]')).toEqual('(square jazz (fast hh 2))')); - it('should desugar , seq', () => expect(desguar('[bd, hh]')).toEqual('(stack bd hh)')); - it('should desugar , seq 2', () => expect(desguar('[bd, hh oh]')).toEqual('(stack bd (seq hh oh))')); - it('should desugar , seq 3', () => expect(desguar('[bd cp, hh oh]')).toEqual('(stack (seq bd cp) (seq hh oh))')); - it('should desugar , cat', () => expect(desguar('')).toEqual('(stack bd hh)')); - it('should desugar , cat 2', () => expect(desguar('')).toEqual('(stack bd (cat hh oh))')); - it('should desugar , cat 3', () => expect(desguar('')).toEqual('(stack (cat bd cp) (cat hh oh))')); + it('should desugar , square', () => expect(desguar('[bd, hh]')).toEqual('(stack bd hh)')); + it('should desugar , square 2', () => expect(desguar('[bd, hh oh]')).toEqual('(stack bd (square hh oh))')); + it('should desugar , square 3', () => + expect(desguar('[bd cp, hh oh]')).toEqual('(stack (square bd cp) (square hh oh))')); + it('should desugar , angle', () => expect(desguar('')).toEqual('(stack bd hh)')); + it('should desugar , angle 2', () => expect(desguar('')).toEqual('(stack bd (angle hh oh))')); + it('should desugar , angle 3', () => + expect(desguar('')).toEqual('(stack (angle bd cp) (angle hh oh))')); it('should desugar , ()', () => expect(desguar('(s bd, s cp)')).toEqual('(stack (s bd) (s cp))')); - it('should desugar * /', () => expect(desguar('[a b*2 c d/3 e]')).toEqual('(seq a (* b 2) c (/ d 3) e)')); - it('should desugar []*x', () => expect(desguar('[a [b c]*3]')).toEqual('(seq a (* (seq b c) 3))')); + it('should desugar * /', () => expect(desguar('[a b*2 c d/3 e]')).toEqual('(square a (* b 2) c (/ d 3) e)')); + it('should desugar []*x', () => expect(desguar('[a [b c]*3]')).toEqual('(square a (* (square b c) 3))')); it('should desugar x:y', () => expect(desguar('x:y')).toEqual('(: x y)')); it('should desugar x:y:z', () => expect(desguar('x:y:z')).toEqual('(: (: x y) z)')); it('should desugar x:y*x', () => expect(desguar('bd:0*2')).toEqual('(* (: bd 0) 2)')); @@ -106,6 +108,6 @@ describe('mondo sugar', () => { it('should desugar README example', () => expect(desguar('s [bd hh*2 cp.(crush 4) ] . speed .8')).toEqual( - '(speed (s (seq bd (* hh 2) (crush cp 4) (cat mt ht lt))) .8)', + '(speed (s (square bd (* hh 2) (crush cp 4) (angle mt ht lt))) .8)', )); }); diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index e81cfecb7..13b0f602f 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -1,52 +1,56 @@ -import { strudelScope, reify, fast, slow, seq, stepcat } from '@strudel/core'; +import { strudelScope, reify, fast, slow, seq, stepcat, extend, expand, pace } from '@strudel/core'; import { registerLanguage } from '@strudel/transpiler'; import { MondoRunner } from '../mondo/mondo.mjs'; -let runner = new MondoRunner(strudelScope); +const tail = (friend, pat) => pat.fmap((a) => (b) => (Array.isArray(a) ? [...a, b] : [a, b])).appLeft(friend); -strudelScope.leaf = (token, scope) => { - let { value } = token; - // local scope - if (token.type === 'plain' && scope[value]) { - return reify(scope[value]); // -> local scope has no location - } - const [from, to] = token.loc; - if (token.type === 'plain' && strudelScope[value]) { - // what if we want a string that happens to also be a variable name? - // example: "s sine" -> sine is also a variable - return reify(strudelScope[value]).withLoc(from, to); - } - return reify(value).withLoc(from, to); -}; -strudelScope.curly = stepcat; -strudelScope.seq = (...args) => stepcat(...args).setSteps(1); -strudelScope.cat = (...args) => stepcat(...args).pace(1); - -strudelScope.call = (fn, args, name) => { - const [pat, ...rest] = args; - if (!['seq', 'cat', 'stack', 'curly', ':', '..', '!', '@', '%'].includes(name)) { - args = [...rest, pat]; - } - return fn(...args); -}; - -strudelScope['*'] = fast; -strudelScope['/'] = slow; -strudelScope['!'] = (pat, n) => pat.extend(n); -strudelScope['@'] = (pat, n) => pat.expand(n); -strudelScope['%'] = (pat, n) => pat.pace(n); - -// : operator -const tail = (pat, friend) => pat.fmap((a) => (b) => (Array.isArray(a) ? [...a, b] : [a, b])).appLeft(friend); -strudelScope[':'] = tail; - -// .. operator const arrayRange = (start, stop, step = 1) => Array.from({ length: Math.abs(stop - start) / step + 1 }, (_, index) => start < stop ? start + index * step : start - index * step, ); const range = (min, max) => min.squeezeBind((a) => max.bind((b) => seq(...arrayRange(a, b)))); -strudelScope['..'] = range; + +let lib = {}; +lib.curly = stepcat; +lib.square = (...args) => stepcat(...args).setSteps(1); +lib.angle = (...args) => stepcat(...args).pace(1); +lib['*'] = fast; +lib['/'] = slow; +lib['!'] = extend; +lib['@'] = expand; +lib['%'] = pace; +lib[':'] = tail; +lib['..'] = range; + +let runner = new MondoRunner({ + call(name, args, scope) { + console.log('call', name, args, scope); + const fn = lib[name] || strudelScope[name]; + if (!fn) { + throw new Error(`[moundough]: unknown function "${name}"`); + } + if (!['square', 'angle', 'stack', 'curly'].includes(name)) { + // flip args (pat to end) + const [pat, ...rest] = args; + args = [...rest, pat]; + } + return fn(...args); + }, + leaf(token, scope) { + let { value } = token; + // local scope + if (token.type === 'plain' && scope[value]) { + return reify(scope[value]); // -> local scope has no location + } + const [from, to] = token.loc; + if (token.type === 'plain' && strudelScope[value]) { + // what if we want a string that happens to also be a variable name? + // example: "s sine" -> sine is also a variable + return reify(strudelScope[value]).withLoc(from, to); + } + return reify(value).withLoc(from, to); + }, +}); export function mondo(code, offset = 0) { if (Array.isArray(code)) { From 05ae31838e2e108f29c9de3d4a90c7e8dbeddbe1 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 20 Mar 2025 18:01:41 +0100 Subject: [PATCH 038/174] add sin sqr cos aliases --- packages/core/signal.mjs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index aa8dd9a6b..140b12c6c 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -71,25 +71,28 @@ export const sine2 = signal((t) => Math.sin(Math.PI * 2 * t)); /** * A sine signal between 0 and 1. - * * @return {Pattern} + * @synonyms sin * @example * n(sine.segment(16).range(0,15)) * .scale("C:minor") * */ export const sine = sine2.fromBipolar(); +export const sin = sine; /** * A cosine signal between 0 and 1. * * @return {Pattern} + * @synonyms cos * @example * n(stack(sine,cosine).segment(16).range(0,15)) * .scale("C:minor") * */ export const cosine = sine._early(Fraction(1).div(4)); +export const cos = cosine; /** * A cosine signal between -1 and 1 (like `cosine`, but bipolar). @@ -102,11 +105,13 @@ export const cosine2 = sine2._early(Fraction(1).div(4)); * A square signal between 0 and 1. * * @return {Pattern} + * @synonyms sqr * @example * n(square.segment(4).range(0,7)).scale("C:minor") * */ export const square = signal((t) => Math.floor((t * 2) % 2)); +export const sqr = square; /** * A square signal between -1 and 1 (like `square`, but bipolar). From 989fdfa20bc3633389cfbb47e4a75ada603f6f11 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 20 Mar 2025 21:18:32 +0100 Subject: [PATCH 039/174] transpiler: add mechanism to register custom mini language --- packages/transpiler/transpiler.mjs | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/packages/transpiler/transpiler.mjs b/packages/transpiler/transpiler.mjs index 0ee371e6a..17f5ae30b 100644 --- a/packages/transpiler/transpiler.mjs +++ b/packages/transpiler/transpiler.mjs @@ -29,8 +29,15 @@ export function transpiler(input, options = {}) { let miniLocations = []; const collectMiniLocations = (value, node) => { - const leafLocs = getLeafLocations(`"${value}"`, node.start, input); - miniLocations = miniLocations.concat(leafLocs); + const minilang = languages.get('minilang'); + if (minilang) { + const code = `[${value}]`; + const locs = minilang.getLocations(code, node.start); + miniLocations = miniLocations.concat(locs); + } else { + const leafLocs = getLeafLocations(`"${value}"`, node.start, input); + miniLocations = miniLocations.concat(leafLocs); + } }; let widgets = []; @@ -142,11 +149,18 @@ function isBackTickString(node, parent) { function miniWithLocation(value, node) { const { start: fromOffset } = node; + + const minilang = languages.get('minilang'); + let name = 'm'; + if (minilang && minilang.name) { + name = minilang.name; // name is expected to be exported from the package of the minilang + } + return { type: 'CallExpression', callee: { type: 'Identifier', - name: 'm', + name, }, arguments: [ { type: 'Literal', value }, From ac6472a43aaa9eadfd8334cec5e1cf4098680670 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 20 Mar 2025 21:19:21 +0100 Subject: [PATCH 040/174] mondo: mondo as minilang (inactive) --- packages/mondough/mondough.mjs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index 13b0f602f..26c9785e0 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -24,7 +24,6 @@ lib['..'] = range; let runner = new MondoRunner({ call(name, args, scope) { - console.log('call', name, args, scope); const fn = lib[name] || strudelScope[name]; if (!fn) { throw new Error(`[moundough]: unknown function "${name}"`); @@ -60,7 +59,19 @@ export function mondo(code, offset = 0) { return pat.markcss('color: var(--foreground);text-decoration:underline'); } +let getLocations = (code, offset) => runner.parser.get_locations(code, offset); + +export const mondi = (str, offset) => { + const code = `[${str}]`; + return mondo(code, offset); +}; + // tell transpiler how to get locations for mondo`` calls registerLanguage('mondo', { - getLocations: (code, offset) => runner.parser.get_locations(code, offset), + getLocations, }); +// uncomment the following to use mondo as mini notation language +/* registerLanguage('minilang', { + name: 'mondi', + getLocations, +}); */ From c33cfc78c5c29216c6d8ad4d4603c39b097b7cc9 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 20 Mar 2025 21:19:47 +0100 Subject: [PATCH 041/174] waveform aliases: tri, sqr, saw, sin --- packages/superdough/synth.mjs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 86c073d0b..569da5560 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -1,5 +1,5 @@ import { clamp, midiToFreq, noteToMidi } from './util.mjs'; -import { registerSound, getAudioContext } from './superdough.mjs'; +import { registerSound, getAudioContext, soundMap } from './superdough.mjs'; import { applyFM, gainNode, @@ -27,6 +27,12 @@ const getFrequencyFromValue = (value) => { }; const waveforms = ['triangle', 'square', 'sawtooth', 'sine']; +const waveformAliases = [ + ['tri', 'triangle'], + ['sqr', 'square'], + ['saw', 'sawtooth'], + ['sin', 'sine'], +]; const noises = ['pink', 'white', 'brown', 'crackle']; export function registerSynthSounds() { @@ -235,6 +241,7 @@ export function registerSynthSounds() { { type: 'synth', prebake: true }, ); }); + waveformAliases.forEach(([alias, actual]) => soundMap.set({ ...soundMap.get(), [alias]: soundMap.get()[actual] })); } export function waveformN(partials, type) { From 12bb82d29564b75a51c3b7360107e081bd350faa Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 21 Mar 2025 11:10:06 +0100 Subject: [PATCH 042/174] add chooseIn + chooseOut --- packages/core/signal.mjs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index 140b12c6c..64a177253 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -403,6 +403,10 @@ export const chooseInWith = (pat, xs) => { */ export const choose = (...xs) => chooseWith(rand, xs); +// todo: doc +export const chooseIn = (...xs) => chooseInWith(rand, xs); +export const chooseOut = choose; + /** * Chooses from the given list of values (or patterns of values), according * to the pattern that the method is called on. The pattern should be in From cf1f4ec35c065870d8fcc3e4cdaea02bdbfcff68 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 21 Mar 2025 11:10:25 +0100 Subject: [PATCH 043/174] fix: patterns without structure would error on draw --- packages/draw/draw.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/draw/draw.mjs b/packages/draw/draw.mjs index 0576c297b..6edca9882 100644 --- a/packages/draw/draw.mjs +++ b/packages/draw/draw.mjs @@ -150,7 +150,7 @@ export class Drawer { this.lastFrame = phase; this.visibleHaps = (this.visibleHaps || []) // filter out haps that are too far in the past (think left edge of screen for pianoroll) - .filter((h) => h.endClipped >= phase - lookbehind - lookahead) + .filter((h) => h.whole && h.endClipped >= phase - lookbehind - lookahead) // add new haps with onset (think right edge bars scrolling in) .concat(haps.filter((h) => h.hasOnset())); const time = phase - lookahead; @@ -175,7 +175,7 @@ export class Drawer { // +0.1 = workaround for weird holes in query.. const [begin, end] = [Math.max(t, 0), t + lookahead + 0.1]; // remove all future haps - this.visibleHaps = this.visibleHaps.filter((h) => h.whole.begin < t); + this.visibleHaps = this.visibleHaps.filter((h) => h.whole?.begin < t); this.painters = []; // will get populated by .onPaint calls attached to the pattern // query future haps const futureHaps = scheduler.pattern.queryArc(begin, end, { painters: this.painters }); From 6fd89ddee7095f955dadc05e09a41a866b3c807b Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 21 Mar 2025 11:10:43 +0100 Subject: [PATCH 044/174] mondo: add | and ? --- packages/mondo/mondo.mjs | 12 +++++++----- packages/mondough/mondough.mjs | 17 ++++++++++++++++- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 19ed27e95..815450263 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -19,9 +19,10 @@ export class MondoParser { open_curly: /^\{/, close_curly: /^\}/, number: /^-?[0-9]*\.?[0-9]+/, // before pipe! - op: /^[*/:!@%]|^\.{2}/, // * / : ! @ % .. + op: /^[*/:!@%?]|^\.{2}/, // * / : ! @ % ? .. pipe: /^\./, stack: /^[,$]/, + or: /^[|]/, plain: /^[a-zA-Z0-9-~_^]+/, }; // matches next token @@ -125,9 +126,9 @@ export class MondoParser { chunks.push(children); return chunks; } - desugar_stack(children, sequence_type) { + desugar_split(children, split_type, sequence_type) { // children is expected to contain square or angle as first item - const chunks = this.split_children(children, 'stack', sequence_type); + const chunks = this.split_children(children, split_type, sequence_type); if (!chunks.length) { return this.desugar_children(children); } @@ -147,7 +148,7 @@ export class MondoParser { return { type: 'list', children: chunk }; } }); - return [{ type: 'plain', value: 'stack' }, ...args]; + return [{ type: 'plain', value: split_type }, ...args]; } // prevents to get a list, e.g. ((x y)) => (x y) unwrap_children(children) { @@ -241,7 +242,8 @@ export class MondoParser { } desugar(children, type) { // not really needed but more readable and might be extended in the future - children = this.desugar_stack(children, type); + children = this.desugar_split(children, 'or', type); + children = this.desugar_split(children, 'stack', type); return children; } parse_list() { diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index 26c9785e0..dcfb1c3c2 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -1,4 +1,16 @@ -import { strudelScope, reify, fast, slow, seq, stepcat, extend, expand, pace } from '@strudel/core'; +import { + strudelScope, + reify, + fast, + slow, + seq, + stepcat, + extend, + expand, + pace, + chooseIn, + degradeBy, +} from '@strudel/core'; import { registerLanguage } from '@strudel/transpiler'; import { MondoRunner } from '../mondo/mondo.mjs'; @@ -19,8 +31,11 @@ lib['/'] = slow; lib['!'] = extend; lib['@'] = expand; lib['%'] = pace; +lib['?'] = degradeBy; // todo: default 0.5 not working.. lib[':'] = tail; lib['..'] = range; +lib['or'] = (...children) => chooseIn(...children); // always has structure but is cyclewise.. e.g. "s oh*8.dec[.04 | .5]" +//lib['or'] = (...children) => chooseOut(...children); // "s oh*8.dec[.04 | .5]" is better but "dec[.04 | .5].s oh*8" has no struct let runner = new MondoRunner({ call(name, args, scope) { From bd93e441546cd4a9a28dc4b121413f149fbf9b61 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 21 Mar 2025 22:34:53 +0100 Subject: [PATCH 045/174] mondo: fix combination of | and , --- packages/mondo/mondo.mjs | 36 +++++++++++++++--------------- packages/mondo/test/mondo.test.mjs | 3 +++ 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 815450263..ba1b3c568 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -107,11 +107,7 @@ export class MondoParser { return children; } // Token[] => Token[][] . returns empty list if type not found - split_children(children, split_type, sequence_type) { - if (sequence_type) { - // if given, the first child is ignored - children = children.slice(1); - } + split_children(children, split_type) { const chunks = []; while (true) { let commaIndex = children.findIndex((child) => child.type === split_type); @@ -126,11 +122,10 @@ export class MondoParser { chunks.push(children); return chunks; } - desugar_split(children, split_type, sequence_type) { - // children is expected to contain square or angle as first item - const chunks = this.split_children(children, split_type, sequence_type); + desugar_split(children, split_type, next) { + const chunks = this.split_children(children, split_type); if (!chunks.length) { - return this.desugar_children(children); + return next(children); } // collect args of stack function const args = chunks.map((chunk) => { @@ -139,12 +134,7 @@ export class MondoParser { return chunk[0]; } else { // chunks of multiple args - if (sequence_type) { - // if given, each chunk needs to be prefixed - // [a b, c d] => (stack (square a b) (square c d)) - chunk = [{ type: 'plain', value: sequence_type }, ...chunk]; - } - chunk = this.desugar_children(chunk); + chunk = next(chunk); return { type: 'list', children: chunk }; } }); @@ -241,9 +231,19 @@ export class MondoParser { return children; } desugar(children, type) { - // not really needed but more readable and might be extended in the future - children = this.desugar_split(children, 'or', type); - children = this.desugar_split(children, 'stack', type); + // if type is given, the first element is expected to contain it as plain value + // e.g. with (square a b, c), we want to split (a b, c) and ignore "square" + children = type ? children.slice(1) : children; + children = this.desugar_split(children, 'stack', (children) => + this.desugar_split(children, 'or', (children) => { + // chunks of multiple args + if (type) { + // the type we've removed before splitting needs to be added back + children = [{ type: 'plain', value: type }, ...children]; + } + return this.desugar_children(children); + }), + ); return children; } parse_list() { diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index 1b9a41e3d..c51844be0 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -90,6 +90,9 @@ describe('mondo sugar', () => { it('should desugar . ()', () => expect(desguar('[jazz hh.(fast 2)]')).toEqual('(square jazz (fast hh 2))')); + it('should desugar , |', () => expect(desguar('[bd, hh | oh]')).toEqual('(stack bd (or hh oh))')); + it('should desugar , | of []', () => + expect(desguar('[bd, hh | [oh rim]]')).toEqual('(stack bd (or hh (square oh rim)))')); it('should desugar , square', () => expect(desguar('[bd, hh]')).toEqual('(stack bd hh)')); it('should desugar , square 2', () => expect(desguar('[bd, hh oh]')).toEqual('(stack bd (square hh oh))')); it('should desugar , square 3', () => From 5d4ef46ac7255e9440d3303fd253cfef0ab8e4e4 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 21 Mar 2025 22:43:39 +0100 Subject: [PATCH 046/174] mondo cleanup --- packages/mondo/mondo.mjs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index ba1b3c568..da969c231 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -217,10 +217,6 @@ export class MondoParser { } return children; } - flip_call(children) { - let [name, first, ...rest] = children; - return [name, ...rest, first]; - } parse_pair(open_type, close_type) { this.consume(open_type); const children = []; @@ -340,7 +336,6 @@ export class MondoRunner { if (name === 'lambda') { const [_, args, body] = ast.children; const argNames = args.children.map((child) => child.value); - // console.log('lambda', argNames, body.children); return (x) => { scope = { [argNames[0]]: x, // TODO: merge scope... + support multiple args @@ -354,7 +349,6 @@ export class MondoRunner { if (arg.type === 'list') { return this.call(arg, scope); } - if (arg.type === 'number') { arg.value = Number(arg.value); } else if (['quotes_double', 'quotes_single'].includes(arg.type)) { @@ -363,7 +357,6 @@ export class MondoRunner { return this.lib.leaf(arg, scope); }); - // look up function in lib return this.lib.call(name, args, scope); } } From f71db4aeed3907e3a4e611100ef746fcbf5cdcf3 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 21 Mar 2025 23:50:14 +0100 Subject: [PATCH 047/174] mondo: flip pipe order: now pining to the end of the function... --- packages/mondo/mondo.mjs | 67 +++++++++--------------------- packages/mondo/test/mondo.test.mjs | 28 ++++++------- packages/mondough/mondough.mjs | 5 --- 3 files changed, 34 insertions(+), 66 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index da969c231..1a211e609 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -106,25 +106,22 @@ export class MondoParser { children = this.resolve_pipes(children); return children; } - // Token[] => Token[][] . returns empty list if type not found + // Token[] => Token[][], e.g. (x , y z) => [['x'],['y','z']] split_children(children, split_type) { const chunks = []; while (true) { - let commaIndex = children.findIndex((child) => child.type === split_type); - if (commaIndex === -1) break; - const chunk = children.slice(0, commaIndex); + let splitIndex = children.findIndex((child) => child.type === split_type); + if (splitIndex === -1) break; + const chunk = children.slice(0, splitIndex); chunk.length && chunks.push(chunk); - children = children.slice(commaIndex + 1); - } - if (!chunks.length) { - return []; + children = children.slice(splitIndex + 1); } chunks.push(children); return chunks; } desugar_split(children, split_type, next) { const chunks = this.split_children(children, split_type); - if (!chunks.length) { + if (chunks.length === 1) { return next(children); } // collect args of stack function @@ -168,7 +165,8 @@ export class MondoParser { children[opIndex] = op; continue; } - const call = { type: 'list', children: [op, left, right] }; + //const call = { type: 'list', children: [op, left, right] }; + const call = { type: 'list', children: [op, right, left] }; // insert call while keeping other siblings children = [...children.slice(0, opIndex - 1), call, ...children.slice(opIndex + 2)]; children = this.unwrap_children(children); @@ -176,46 +174,21 @@ export class MondoParser { return children; } resolve_pipes(children) { - while (true) { - let pipeIndex = children.findIndex((child) => child.type === 'pipe'); - // no pipe => we're done - if (pipeIndex === -1) break; - // pipe up front => lambda - if (pipeIndex === 0) { - // . as lambda: (.fast 2) = (lambda (_) (fast _ 2)) - const args = [{ type: 'plain', value: '_' }]; - const body = this.desugar([args[0], ...children]); - return [ - { type: 'plain', value: 'lambda' }, - { type: 'list', children: args }, - { type: 'list', children: body }, - ]; - } - const rightSide = children.slice(pipeIndex + 2); - const right = children[pipeIndex + 1]; - if (right.type === 'list') { - // apply function only to left sibling (high precedence) - // s jazz.(fast 2) => s (fast jazz 2) - const [callee, ...rest] = right.children; - const leftSide = children.slice(0, pipeIndex - 1); - const left = children[pipeIndex - 1]; - let args = [callee, left, ...rest]; - const call = { type: 'list', children: args }; - children = [...leftSide, call, ...rightSide]; + let chunks = this.split_children(children, 'pipe'); + while (chunks.length > 1) { + let [left, right, ...rest] = chunks; + if (right.length && right[0].type === 'list') { + // s jazz hh.(fast 2) => s jazz (fast 2 hh) + const target = left[left.length - 1]; // hh + const call = { type: 'list', children: [...right[0].children, target] }; + chunks = [[...left.slice(0, -1), call, ...right.slice(1)], ...rest]; // jazz (fast 2 hh) } else { - // apply function to all left siblings (low precedence) - // s jazz . fast 2 => fast (s jazz) 2 - let leftSide = children.slice(0, pipeIndex); - if (leftSide.length === 1) { - leftSide = leftSide[0]; - } else { - // wrap in (..) if multiple items on the left side - leftSide = { type: 'list', children: leftSide }; - } - children = [right, leftSide, ...rightSide]; + //s jazz hh.fast 2 => (fast 2 (s jazz hh)) + const call = left.length > 1 ? { type: 'list', children: left } : left[0]; + chunks = [[...right, call], ...rest]; } } - return children; + return chunks[0]; } parse_pair(open_type, close_type) { this.consume(open_type); diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index c51844be0..342a3f9fd 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -80,15 +80,15 @@ describe('mondo sugar', () => { it('should desugar mixed [] <>', () => expect(desguar('[a ]')).toEqual('(square a (angle b c))')); it('should desugar mixed <> []', () => expect(desguar('')).toEqual('(angle a (square b c))')); - it('should desugar .', () => expect(desguar('s jazz . fast 2')).toEqual('(fast (s jazz) 2)')); - it('should desugar . square', () => expect(desguar('[bd cp . fast 2]')).toEqual('(fast (square bd cp) 2)')); - it('should desugar . twice', () => expect(desguar('s jazz . fast 2 . slow 2')).toEqual('(slow (fast (s jazz) 2) 2)')); - it('should desugar . nested', () => expect(desguar('(s cp . fast 2)')).toEqual('(fast (s cp) 2)')); - it('should desugar . within []', () => expect(desguar('[bd cp . fast 2]')).toEqual('(fast (square bd cp) 2)')); + it('should desugar .', () => expect(desguar('s jazz . fast 2')).toEqual('(fast 2 (s jazz))')); + it('should desugar . square', () => expect(desguar('[bd cp . fast 2]')).toEqual('(fast 2 (square bd cp))')); + it('should desugar . twice', () => expect(desguar('s jazz . fast 2 . slow 2')).toEqual('(slow 2 (fast 2 (s jazz)))')); + it('should desugar . nested', () => expect(desguar('(s cp . fast 2)')).toEqual('(fast 2 (s cp))')); + it('should desugar . within []', () => expect(desguar('[bd cp . fast 2]')).toEqual('(fast 2 (square bd cp))')); it('should desugar . within , within []', () => - expect(desguar('[bd cp . fast 2, x]')).toEqual('(stack (fast (square bd cp) 2) x)')); + expect(desguar('[bd cp . fast 2, x]')).toEqual('(stack (fast 2 (square bd cp)) x)')); - it('should desugar . ()', () => expect(desguar('[jazz hh.(fast 2)]')).toEqual('(square jazz (fast hh 2))')); + it('should desugar . ()', () => expect(desguar('[jazz hh.(fast 2)]')).toEqual('(square jazz (fast 2 hh))')); it('should desugar , |', () => expect(desguar('[bd, hh | oh]')).toEqual('(stack bd (or hh oh))')); it('should desugar , | of []', () => @@ -102,15 +102,15 @@ describe('mondo sugar', () => { it('should desugar , angle 3', () => expect(desguar('')).toEqual('(stack (angle bd cp) (angle hh oh))')); it('should desugar , ()', () => expect(desguar('(s bd, s cp)')).toEqual('(stack (s bd) (s cp))')); - it('should desugar * /', () => expect(desguar('[a b*2 c d/3 e]')).toEqual('(square a (* b 2) c (/ d 3) e)')); - it('should desugar []*x', () => expect(desguar('[a [b c]*3]')).toEqual('(square a (* (square b c) 3))')); - it('should desugar x:y', () => expect(desguar('x:y')).toEqual('(: x y)')); - it('should desugar x:y:z', () => expect(desguar('x:y:z')).toEqual('(: (: x y) z)')); - it('should desugar x:y*x', () => expect(desguar('bd:0*2')).toEqual('(* (: bd 0) 2)')); - it('should desugar a..b', () => expect(desguar('0..2')).toEqual('(.. 0 2)')); + it('should desugar * /', () => expect(desguar('[a b*2 c d/3 e]')).toEqual('(square a (* 2 b) c (/ 3 d) e)')); + it('should desugar []*x', () => expect(desguar('[a [b c]*3]')).toEqual('(square a (* 3 (square b c)))')); + it('should desugar x:y', () => expect(desguar('x:y')).toEqual('(: y x)')); + it('should desugar x:y:z', () => expect(desguar('x:y:z')).toEqual('(: z (: y x))')); + it('should desugar x:y*x', () => expect(desguar('bd:0*2')).toEqual('(* 2 (: 0 bd))')); + it('should desugar a..b', () => expect(desguar('0..2')).toEqual('(.. 2 0)')); it('should desugar README example', () => expect(desguar('s [bd hh*2 cp.(crush 4) ] . speed .8')).toEqual( - '(speed (s (square bd (* hh 2) (crush cp 4) (angle mt ht lt))) .8)', + '(speed .8 (s (square bd (* 2 hh) (crush 4 cp) (angle mt ht lt))))', )); }); diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index dcfb1c3c2..0db3667c3 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -43,11 +43,6 @@ let runner = new MondoRunner({ if (!fn) { throw new Error(`[moundough]: unknown function "${name}"`); } - if (!['square', 'angle', 'stack', 'curly'].includes(name)) { - // flip args (pat to end) - const [pat, ...rest] = args; - args = [...rest, pat]; - } return fn(...args); }, leaf(token, scope) { From 492271d786f57921ee3bff62e30f386dd87b698d Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 22 Mar 2025 23:28:57 +0100 Subject: [PATCH 048/174] mondo: support $ tidal style --- packages/mondo/mondo.mjs | 19 +++++++++++++++---- packages/mondo/test/mondo.test.mjs | 3 +++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 1a211e609..ce9e960a1 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -20,8 +20,9 @@ export class MondoParser { close_curly: /^\}/, number: /^-?[0-9]*\.?[0-9]+/, // before pipe! op: /^[*/:!@%?]|^\.{2}/, // * / : ! @ % ? .. + dollar: /^\$/, pipe: /^\./, - stack: /^[,$]/, + stack: /^[,]/, or: /^[|]/, plain: /^[a-zA-Z0-9-~_^]+/, }; @@ -103,7 +104,7 @@ export class MondoParser { } desugar_children(children) { children = this.resolve_ops(children); - children = this.resolve_pipes(children); + children = this.resolve_pipes(children, (children) => this.resolve_dollars(children)); return children; } // Token[] => Token[][], e.g. (x , y z) => [['x'],['y','z']] @@ -173,7 +174,7 @@ export class MondoParser { } return children; } - resolve_pipes(children) { + resolve_pipes(children, next) { let chunks = this.split_children(children, 'pipe'); while (chunks.length > 1) { let [left, right, ...rest] = chunks; @@ -184,10 +185,20 @@ export class MondoParser { chunks = [[...left.slice(0, -1), call, ...right.slice(1)], ...rest]; // jazz (fast 2 hh) } else { //s jazz hh.fast 2 => (fast 2 (s jazz hh)) - const call = left.length > 1 ? { type: 'list', children: left } : left[0]; + const call = left.length > 1 ? { type: 'list', children: next(left) } : left[0]; chunks = [[...right, call], ...rest]; } } + return next(chunks[0]); + } + resolve_dollars(children) { + let chunks = this.split_children(children, 'dollar'); + while (chunks.length > 1) { + let [left, right, ...rest] = chunks; + //fast 2 $ s jazz hh => (fast 2 (s jazz hh)) + const call = right.length > 1 ? { type: 'list', children: right } : right[0]; + chunks = [[...left, call], ...rest]; + } return chunks[0]; } parse_pair(open_type, close_type) { diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index 342a3f9fd..0e859a8b2 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -108,6 +108,9 @@ describe('mondo sugar', () => { it('should desugar x:y:z', () => expect(desguar('x:y:z')).toEqual('(: z (: y x))')); it('should desugar x:y*x', () => expect(desguar('bd:0*2')).toEqual('(* 2 (: 0 bd))')); it('should desugar a..b', () => expect(desguar('0..2')).toEqual('(.. 2 0)')); + it('should desugar x $ y', () => expect(desguar('x $ y')).toEqual('(x y)')); + it('should desugar x $ y z', () => expect(desguar('x $ y z')).toEqual('(x (y z))')); + it('should desugar x $ y . z', () => expect(desguar('x $ y . z')).toEqual('(z (x y))')); it('should desugar README example', () => expect(desguar('s [bd hh*2 cp.(crush 4) ] . speed .8')).toEqual( From bca16cdf99614dbc8f1ef4728a13957045752075 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 22 Mar 2025 23:51:23 +0100 Subject: [PATCH 049/174] mondo: rename resolve_ -> desugar_ --- packages/mondo/mondo.mjs | 15 ++++++--------- packages/mondo/test/mondo.test.mjs | 1 + 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index ce9e960a1..a14dcc5de 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -102,11 +102,6 @@ export class MondoParser { } return this.consume(next); } - desugar_children(children) { - children = this.resolve_ops(children); - children = this.resolve_pipes(children, (children) => this.resolve_dollars(children)); - return children; - } // Token[] => Token[][], e.g. (x , y z) => [['x'],['y','z']] split_children(children, split_type) { const chunks = []; @@ -145,7 +140,7 @@ export class MondoParser { } return children; } - resolve_ops(children) { + desugar_ops(children) { while (true) { let opIndex = children.findIndex((child) => child.type === 'op'); if (opIndex === -1) break; @@ -174,7 +169,7 @@ export class MondoParser { } return children; } - resolve_pipes(children, next) { + desugar_pipes(children, next) { let chunks = this.split_children(children, 'pipe'); while (chunks.length > 1) { let [left, right, ...rest] = chunks; @@ -191,7 +186,7 @@ export class MondoParser { } return next(chunks[0]); } - resolve_dollars(children) { + desugar_dollars(children) { let chunks = this.split_children(children, 'dollar'); while (chunks.length > 1) { let [left, right, ...rest] = chunks; @@ -221,7 +216,9 @@ export class MondoParser { // the type we've removed before splitting needs to be added back children = [{ type: 'plain', value: type }, ...children]; } - return this.desugar_children(children); + children = this.desugar_ops(children); + children = this.desugar_pipes(children, (children) => this.desugar_dollars(children)); + return children; }), ); return children; diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index 0e859a8b2..f302174e8 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -104,6 +104,7 @@ describe('mondo sugar', () => { it('should desugar , ()', () => expect(desguar('(s bd, s cp)')).toEqual('(stack (s bd) (s cp))')); it('should desugar * /', () => expect(desguar('[a b*2 c d/3 e]')).toEqual('(square a (* 2 b) c (/ 3 d) e)')); it('should desugar []*x', () => expect(desguar('[a [b c]*3]')).toEqual('(square a (* 3 (square b c)))')); + it('should desugar []*', () => expect(desguar('[a b*<2 3> c]')).toEqual('(square a (* (angle 2 3) b) c)')); it('should desugar x:y', () => expect(desguar('x:y')).toEqual('(: y x)')); it('should desugar x:y:z', () => expect(desguar('x:y:z')).toEqual('(: z (: y x))')); it('should desugar x:y*x', () => expect(desguar('bd:0*2')).toEqual('(* 2 (: 0 bd))')); From 6d214564230d77dbbb1e18df91a7a1a3884ac90f Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 23 Mar 2025 11:18:18 +0100 Subject: [PATCH 050/174] mondo: allow # character in plain values (for sharps) --- packages/mondo/mondo.mjs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index a14dcc5de..cc05e131e 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -24,7 +24,7 @@ export class MondoParser { pipe: /^\./, stack: /^[,]/, or: /^[|]/, - plain: /^[a-zA-Z0-9-~_^]+/, + plain: /^[a-zA-Z0-9-~_^#]+/, }; // matches next token next_token(code, offset = 0) { @@ -109,7 +109,7 @@ export class MondoParser { let splitIndex = children.findIndex((child) => child.type === split_type); if (splitIndex === -1) break; const chunk = children.slice(0, splitIndex); - chunk.length && chunks.push(chunk); + chunks.push(chunk); children = children.slice(splitIndex + 1); } chunks.push(children); @@ -173,6 +173,16 @@ export class MondoParser { let chunks = this.split_children(children, 'pipe'); while (chunks.length > 1) { let [left, right, ...rest] = chunks; + if (!left.length) { + // . as lambda: (.fast 2) = (lambda (_) (fast _ 2)) + const args = [{ type: 'plain', value: '_' }]; + const body = this.desugar([args[0], ...children]); + return [ + { type: 'plain', value: 'lambda' }, + { type: 'list', children: args }, + { type: 'list', children: body }, + ]; + } if (right.length && right[0].type === 'list') { // s jazz hh.(fast 2) => s jazz (fast 2 hh) const target = left[left.length - 1]; // hh From 5eabcf061875c5241ccd84add4e9b3c5b111bc17 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 23 Mar 2025 22:47:33 +0100 Subject: [PATCH 051/174] mondo mode for StrudelMirror / repl / mini repl --- packages/codemirror/codemirror.mjs | 5 +++-- packages/core/repl.mjs | 5 +++++ website/src/config.ts | 1 + website/src/docs/MiniRepl.jsx | 2 ++ 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs index e96b533ec..193d96b55 100644 --- a/packages/codemirror/codemirror.mjs +++ b/packages/codemirror/codemirror.mjs @@ -62,7 +62,7 @@ export const codemirrorSettings = persistentAtom('codemirror-settings', defaultS }); // https://codemirror.net/docs/guide/ -export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, root }) { +export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, root, mondo }) { const settings = codemirrorSettings.get(); const initialSettings = Object.keys(compartments).map((key) => compartments[key].of(extensions[key](parseBooleans(settings[key]))), @@ -75,7 +75,7 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo /* search(), highlightSelectionMatches(), */ ...initialSettings, - javascript(), + mondo ? [] : javascript(), sliderPlugin, widgetPlugin, // indentOnInput(), // works without. already brought with javascript extension? @@ -209,6 +209,7 @@ export class StrudelMirror { }, onEvaluate: () => this.evaluate(), onStop: () => this.stop(), + mondo: replOptions.mondo, }); const cmEditor = this.root.querySelector('.cm-editor'); if (cmEditor) { diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index e703909ff..147155fbe 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -21,6 +21,7 @@ export function repl({ setInterval, clearInterval, id, + mondo = false, }) { const state = { schedulerError: undefined, @@ -180,6 +181,10 @@ export function repl({ setTime(() => scheduler.now()); // TODO: refactor? await beforeEval?.({ code }); shouldHush && hush(); + + if (mondo) { + code = `mondolang\`${code}\``; + } let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions); if (Object.keys(pPatterns).length) { let patterns = Object.values(pPatterns); diff --git a/website/src/config.ts b/website/src/config.ts index 2f499d55f..124cfb3f6 100644 --- a/website/src/config.ts +++ b/website/src/config.ts @@ -81,6 +81,7 @@ export const SIDEBAR: Sidebar = { { text: 'Visual Feedback', link: 'learn/visual-feedback' }, { text: 'Offline', link: 'learn/pwa' }, { text: 'Patterns', link: 'technical-manual/patterns' }, + { text: 'Mondo Notation', link: 'learn/mondo-notation' }, { text: 'Music metadata', link: 'learn/metadata' }, { text: 'CSound', link: 'learn/csound' }, { text: 'Hydra', link: 'learn/hydra' }, diff --git a/website/src/docs/MiniRepl.jsx b/website/src/docs/MiniRepl.jsx index 0f3d3ce10..09ebb7692 100644 --- a/website/src/docs/MiniRepl.jsx +++ b/website/src/docs/MiniRepl.jsx @@ -30,6 +30,7 @@ export function MiniRepl({ maxHeight, autodraw, drawTime, + mondo = false, }) { const code = tunes ? tunes[0] : tune; const id = useMemo(() => s4(), []); @@ -85,6 +86,7 @@ export function MiniRepl({ }, beforeStart: () => audioReady, afterEval: ({ code }) => setVersionDefaultsFrom(code), + mondo, }); // init settings editor.setCode(code); From 51e3aa257ccbf1e1f21a4caadcd9cf7d25ce4268 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 23 Mar 2025 22:49:03 +0100 Subject: [PATCH 052/174] mondolang function for mondo repl + fix rests --- packages/mondough/mondough.mjs | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index 0db3667c3..f42f0ca76 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -10,6 +10,7 @@ import { pace, chooseIn, degradeBy, + silence, } from '@strudel/core'; import { registerLanguage } from '@strudel/transpiler'; import { MondoRunner } from '../mondo/mondo.mjs'; @@ -20,9 +21,11 @@ const arrayRange = (start, stop, step = 1) => Array.from({ length: Math.abs(stop - start) / step + 1 }, (_, index) => start < stop ? start + index * step : start - index * step, ); -const range = (min, max) => min.squeezeBind((a) => max.bind((b) => seq(...arrayRange(a, b)))); +const range = (max, min) => min.squeezeBind((a) => max.bind((b) => seq(...arrayRange(a, b)))); let lib = {}; +lib['-'] = silence; +lib['~'] = silence; lib.curly = stepcat; lib.square = (...args) => stepcat(...args).setSteps(1); lib.angle = (...args) => stepcat(...args).pace(1); @@ -46,14 +49,15 @@ let runner = new MondoRunner({ return fn(...args); }, leaf(token, scope) { - let { value } = token; + let { value, type } = token; // local scope - if (token.type === 'plain' && scope[value]) { + if (type === 'plain' && scope[value]) { return reify(scope[value]); // -> local scope has no location } const [from, to] = token.loc; - if (token.type === 'plain' && strudelScope[value]) { - // what if we want a string that happens to also be a variable name? + const variable = lib[value] ?? strudelScope[value]; + if (type === 'plain' && typeof variable !== 'undefined') { + // problem: collisions when we want a string that happens to also be a variable name // example: "s sine" -> sine is also a variable return reify(strudelScope[value]).withLoc(from, to); } @@ -66,7 +70,7 @@ export function mondo(code, offset = 0) { code = code.join(''); } const pat = runner.run(code, offset); - return pat.markcss('color: var(--foreground);text-decoration:underline'); + return pat.markcss('color: var(--caret,--foreground);text-decoration:underline'); } let getLocations = (code, offset) => runner.parser.get_locations(code, offset); @@ -80,6 +84,12 @@ export const mondi = (str, offset) => { registerLanguage('mondo', { getLocations, }); + +// this is like mondo, but with a zero offset +export const mondolang = (code) => mondo(code, 0); +registerLanguage('mondolang', { + getLocations: (code) => getLocations(code, 0), +}); // uncomment the following to use mondo as mini notation language /* registerLanguage('minilang', { name: 'mondi', From dd5743ab8c3e0ae057a0859e57badfaf98a00e1e Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 23 Mar 2025 22:49:23 +0100 Subject: [PATCH 053/174] mondo: bring back $ for stacking --- packages/mondo/mondo.mjs | 37 ++++++++++++++++++------------ packages/mondo/test/mondo.test.mjs | 4 ++-- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index cc05e131e..3f0eb0385 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -20,9 +20,9 @@ export class MondoParser { close_curly: /^\}/, number: /^-?[0-9]*\.?[0-9]+/, // before pipe! op: /^[*/:!@%?]|^\.{2}/, // * / : ! @ % ? .. - dollar: /^\$/, + // dollar: /^\$/, pipe: /^\./, - stack: /^[,]/, + stack: /^[,$]/, or: /^[|]/, plain: /^[a-zA-Z0-9-~_^#]+/, }; @@ -121,16 +121,20 @@ export class MondoParser { return next(children); } // collect args of stack function - const args = chunks.map((chunk) => { - if (chunk.length === 1) { - // chunks of one element can be added to the stack as is - return chunk[0]; - } else { + const args = chunks + .map((chunk) => { + if (!chunk.length) { + return; // useful for things like "$ s bd $ s hh*8" (first chunk is empty) + } + if (chunk.length === 1) { + // chunks of one element can be added to the stack as is + return chunk[0]; + } // chunks of multiple args chunk = next(chunk); return { type: 'list', children: chunk }; - } - }); + }) + .filter(Boolean); // ignore empty chunks return [{ type: 'plain', value: split_type }, ...args]; } // prevents to get a list, e.g. ((x y)) => (x y) @@ -169,7 +173,7 @@ export class MondoParser { } return children; } - desugar_pipes(children, next) { + desugar_pipes(children) { let chunks = this.split_children(children, 'pipe'); while (chunks.length > 1) { let [left, right, ...rest] = chunks; @@ -190,13 +194,15 @@ export class MondoParser { chunks = [[...left.slice(0, -1), call, ...right.slice(1)], ...rest]; // jazz (fast 2 hh) } else { //s jazz hh.fast 2 => (fast 2 (s jazz hh)) - const call = left.length > 1 ? { type: 'list', children: next(left) } : left[0]; + // const call = left.length > 1 ? { type: 'list', children: next(left) } : left[0]; + const call = left.length > 1 ? { type: 'list', children: left } : left[0]; chunks = [[...right, call], ...rest]; } } - return next(chunks[0]); + // return next(chunks[0]); + return chunks[0]; } - desugar_dollars(children) { + /* desugar_dollars(children) { let chunks = this.split_children(children, 'dollar'); while (chunks.length > 1) { let [left, right, ...rest] = chunks; @@ -205,7 +211,7 @@ export class MondoParser { chunks = [[...left, call], ...rest]; } return chunks[0]; - } + } */ parse_pair(open_type, close_type) { this.consume(open_type); const children = []; @@ -227,7 +233,8 @@ export class MondoParser { children = [{ type: 'plain', value: type }, ...children]; } children = this.desugar_ops(children); - children = this.desugar_pipes(children, (children) => this.desugar_dollars(children)); + // children = this.desugar_pipes(children, (children) => this.desugar_dollars(children)); + children = this.desugar_pipes(children); return children; }), ); diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index f302174e8..a35eb8aa8 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -109,9 +109,9 @@ describe('mondo sugar', () => { it('should desugar x:y:z', () => expect(desguar('x:y:z')).toEqual('(: z (: y x))')); it('should desugar x:y*x', () => expect(desguar('bd:0*2')).toEqual('(* 2 (: 0 bd))')); it('should desugar a..b', () => expect(desguar('0..2')).toEqual('(.. 2 0)')); - it('should desugar x $ y', () => expect(desguar('x $ y')).toEqual('(x y)')); + /* it('should desugar x $ y', () => expect(desguar('x $ y')).toEqual('(x y)')); it('should desugar x $ y z', () => expect(desguar('x $ y z')).toEqual('(x (y z))')); - it('should desugar x $ y . z', () => expect(desguar('x $ y . z')).toEqual('(z (x y))')); + it('should desugar x $ y . z', () => expect(desguar('x $ y . z')).toEqual('(z (x y))')); */ it('should desugar README example', () => expect(desguar('s [bd hh*2 cp.(crush 4) ] . speed .8')).toEqual( From 3a19e23473299a6664e085f9aa1620a262663f38 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 23 Mar 2025 22:49:56 +0100 Subject: [PATCH 054/174] mondo: doc --- website/src/pages/learn/mondo-notation.mdx | 145 +++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 website/src/pages/learn/mondo-notation.mdx diff --git a/website/src/pages/learn/mondo-notation.mdx b/website/src/pages/learn/mondo-notation.mdx new file mode 100644 index 000000000..caf9f67fe --- /dev/null +++ b/website/src/pages/learn/mondo-notation.mdx @@ -0,0 +1,145 @@ +--- +title: Mondo Notation +layout: ../../layouts/MainLayout.astro +--- + +import { MiniRepl } from '../../docs/MiniRepl'; +import { JsDoc } from '../../docs/JsDoc'; + +# Mondo Notation + +"Mondo Notation" is a new kind of notation that is similar to [Mini Notation](/learn/mini-notation/), but with enough abilities to make it work as a standalone pattern language. +Here's an example: + +/2 +.jux /2 +.rarely (12.note.add) .delay .4 .vib 1:.3 +.fm (sine/4.range .5 4).fmh 2.02 +.lpf (sine/2.range 200 2000).lpq 4 +.s piano .clip 2 +.add (perlin.range 0 .2 .note)`} +/> + +## Calling Functions + +Compared to Mini Notation, the most notable feature of Mondo Notation is the ability to call functions using round brackets: + + + +The first element inside the brackets is the function name. In JS, this would look like: + + + +The outermost parens are not needed, so we can drop them: + + + +## Mini Notation Features + +Besides function calling with round parens, Mondo Notation has a lot in common with Mini Notation: + +- `[]` for 1-cycle sequences +- `<>` for multi-cycle sequences +- `{}` for stepped sequences (more on that later) +- \* => [fast](/learn/time-modifiers/#fast) +- / => [slow](/learn/time-modifiers/#slow) +- , => [stack](/learn/factories/#stack) +- ! => [extend](/learn/stepwise/#extend) +- @ => [expand](/learn/stepwise/#expand) +- % => [pace](/learn/stepwise/#pace) +- ? => [degradeBy](/learn/random-modifiers/#degradeby) (currently requires right operand) +- : => tail (creates a list) +- .. => range (between numbers) +- | => [chooseIn](/learn/random-modifiers/#choose) + +Example: + +`} +/> + +## Chaining Functions + +Similar to how it works in JS, we can chain functions calls with the "." operator: + +*4 +.scale C4:minor +.jux rev +.dec .2 +.delay .5`} +/> + +Here's the same written in JS: + +*4") +.scale("C4:minor") +.jux(rev) +.dec(.2) +.delay(.5)`} +/> + +### Chaining Functions Locally + +A function can be applied "locally" by wrapping it in round parens: + + + +in this case, `delay .6` will only be applied to `cp`. compare this with the JS version: + + + +here we can see much we can save when there's no boundary between mini notation and function calls! + +### Lambda Functions + +Some functions in strudel expect a function as input, for example: + +x.dec(.1))`} /> + +in mondo, the `x=>x.` can be shortened to: + + + +chaining works as expected: + + + +## Strings + +You can use "double quotes" and 'single quotes' to get a string: + + + +## Multiple Patterns + +The `$` sign can be used to separate multiple patterns: + +.voicing +.struct[x - - x - x - -].delay.5`} +/> + +The `$` sign is an alias for `,` so it will create a stack behind the scenes. From 92b5b6538f91a300c1c2ca0aef04d3ed3830b06a Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 23 Mar 2025 23:12:52 +0100 Subject: [PATCH 055/174] mondo: improve doc --- website/src/pages/learn/mondo-notation.mdx | 42 ++++++++++++++++------ 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/website/src/pages/learn/mondo-notation.mdx b/website/src/pages/learn/mondo-notation.mdx index caf9f67fe..6a2f07ff9 100644 --- a/website/src/pages/learn/mondo-notation.mdx +++ b/website/src/pages/learn/mondo-notation.mdx @@ -14,14 +14,20 @@ Here's an example: /2 -.jux /2 -.rarely (12.note.add) .delay .4 .vib 1:.3 -.fm (sine/4.range .5 4).fmh 2.02 -.lpf (sine/2.range 200 2000).lpq 4 -.s piano .clip 2 -.add (perlin.range 0 .2 .note)`} + tune={`$ note c2 .(euclid <3 6 3> <8 16>) . *2 +.s "sine" .add (note [0 <12 24>]*2) +.dec(sine .range .2 2) .room .5 +.lpf(sine/3.range 120 400) +.lpenv(rand .range .5 4) +.lpq(perlin .range 5 12 . * 2) +.dist 1 .fm 4 .fmh 5.01 .fmdecay <.1 .2> +.postgain .6 .delay .1 .clip 5 + +$ s [bd bd bd bd] .bank tr909.clip.5 +.ply<1 [1 [2 4]]> + +$ s oh*4 .press .bank tr909 .speed.8 +.dec <.02 .05>*2 .(add (saw/8.range 0 1))`} /> ## Calling Functions @@ -42,21 +48,29 @@ The outermost parens are not needed, so we can drop them: Besides function calling with round parens, Mondo Notation has a lot in common with Mini Notation: +### Brackets + - `[]` for 1-cycle sequences - `<>` for multi-cycle sequences - `{}` for stepped sequences (more on that later) + +### Infix Operators + - \* => [fast](/learn/time-modifiers/#fast) - / => [slow](/learn/time-modifiers/#slow) -- , => [stack](/learn/factories/#stack) - ! => [extend](/learn/stepwise/#extend) - @ => [expand](/learn/stepwise/#expand) - % => [pace](/learn/stepwise/#pace) - ? => [degradeBy](/learn/random-modifiers/#degradeby) (currently requires right operand) - : => tail (creates a list) - .. => range (between numbers) + +### Separators + +- , => [stack](/learn/factories/#stack) - | => [chooseIn](/learn/random-modifiers/#choose) -Example: +### Example + +In this case, the \*2 will be applied to the whole pattern. + ### Lambda Functions Some functions in strudel expect a function as input, for example: From 222e479e30fede8f8cf567207b0fc691d57508d3 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 23 Mar 2025 23:18:57 +0100 Subject: [PATCH 056/174] mondo: more docs --- website/src/pages/learn/mondo-notation.mdx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/website/src/pages/learn/mondo-notation.mdx b/website/src/pages/learn/mondo-notation.mdx index 6a2f07ff9..aa7904987 100644 --- a/website/src/pages/learn/mondo-notation.mdx +++ b/website/src/pages/learn/mondo-notation.mdx @@ -30,6 +30,15 @@ $ s oh*4 .press .bank tr909 .speed.8 .dec <.02 .05>*2 .(add (saw/8.range 0 1))`} /> +## Mondo in the REPL + +For now, you can only use mondo in the repl like this: + + + +The rest of this site will only use the mondo notation itself. +In the future, the REPL might get a way to use mondo notation directly. + ## Calling Functions Compared to Mini Notation, the most notable feature of Mondo Notation is the ability to call functions using round brackets: @@ -64,9 +73,6 @@ Besides function calling with round parens, Mondo Notation has a lot in common w - ? => [degradeBy](/learn/random-modifiers/#degradeby) (currently requires right operand) - : => tail (creates a list) - .. => range (between numbers) - -### Separators - - , => [stack](/learn/factories/#stack) - | => [chooseIn](/learn/random-modifiers/#choose) From 2938bfd88f9e74449a585d49c41462dda2cb9beb Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 26 Mar 2025 02:46:26 +0100 Subject: [PATCH 057/174] mondo: add highly impractical .(. notation for local infix application --- packages/mondo/README.md | 2 +- packages/mondo/mondo.mjs | 79 ++++++++++++++++++++++++------ packages/mondo/test/mondo.test.mjs | 12 ++++- 3 files changed, 74 insertions(+), 19 deletions(-) diff --git a/packages/mondo/README.md b/packages/mondo/README.md index a6a59dd94..096d77d56 100644 --- a/packages/mondo/README.md +++ b/packages/mondo/README.md @@ -9,7 +9,7 @@ an experimental parser for an *uzulang*, a custom dsl for patterns that can stan import { MondoRunner } from 'uzu' const runner = MondoRunner({ seq, cat, s, crush, speed, '*': fast }); -const pat = runner.run('s [bd hh*2 cp.(crush 4) ] . speed .8') +const pat = runner.run('s [bd hh*2 cp.(.crush 4) ] . speed .8') ``` the above code will create the following call structure: diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 3f0eb0385..70015cc2d 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -63,6 +63,8 @@ export class MondoParser { } // take code, return abstract syntax tree parse(code, offset) { + this.code = code; + this.offset = offset; this.tokens = this.tokenize(code, offset); const expressions = []; while (this.tokens.length) { @@ -165,7 +167,6 @@ export class MondoParser { children[opIndex] = op; continue; } - //const call = { type: 'list', children: [op, left, right] }; const call = { type: 'list', children: [op, right, left] }; // insert call while keeping other siblings children = [...children.slice(0, opIndex - 1), call, ...children.slice(opIndex + 2)]; @@ -173,24 +174,73 @@ export class MondoParser { } return children; } + get_lambda(args, children) { + // (.fast 2) = (lambda (_) (fast _ 2)) + const body = this.desugar(children); + return [ + { type: 'plain', value: 'lambda' }, + { type: 'list', children: args }, + { type: 'list', children: body }, + ]; + } + // inserts target into lambda body + desugar_lambda(lambda, target) { + // lambda looks like return value from this.get_lambda + const [_, args, body] = lambda; + if (args.length > 1) { + throw new Error('desugar_lambda with >1 arg is unsupported rn'); + } + const argNames = args.children.map((child) => child.value); + let desugar = (child) => { + if (child.type === 'plain' && child.value === argNames[0]) { + return target; + } + if (child.type === 'list') { + child.children = child.children.map(desugar); + } + return child; + }; + return desugar(body); + } + // returns location range of given ast (even if desugared) + get_range(ast, range = [Infinity, 0]) { + let union = (a, b) => [Math.min(a[0], b[0]), Math.max(a[1], b[1])]; + if (ast.loc) { + return union(range, ast.loc); + } + if (ast.type !== 'list') { + return range; + } + return ast.children.reduce((range, child) => { + const childrange = this.get_range(child, range); + return union(range, childrange); + }, range); + } + errorhead(ast) { + return `[mondo ${this.get_range(ast)?.join(':') || '?'}]`; + } + // returns original user code where the given ast originates (even if desugared) + get_code_snippet(ast) { + const [min, max] = this.get_range(ast); + return this.code.slice(min - this.offset, max - this.offset); + } desugar_pipes(children) { let chunks = this.split_children(children, 'pipe'); while (chunks.length > 1) { let [left, right, ...rest] = chunks; if (!left.length) { - // . as lambda: (.fast 2) = (lambda (_) (fast _ 2)) - const args = [{ type: 'plain', value: '_' }]; - const body = this.desugar([args[0], ...children]); - return [ - { type: 'plain', value: 'lambda' }, - { type: 'list', children: args }, - { type: 'list', children: body }, - ]; + const arg = { type: 'plain', value: '_' }; + return this.get_lambda([arg], [arg, ...children]); } if (right.length && right[0].type === 'list') { - // s jazz hh.(fast 2) => s jazz (fast 2 hh) + // s jazz hh.(.fast 2) => s jazz (hh.fast 2) = s jazz (fast 2 hh) const target = left[left.length - 1]; // hh - const call = { type: 'list', children: [...right[0].children, target] }; + + if (right[0].children[0].value !== 'lambda') { + const snip = this.get_code_snippet(right[0]); + throw new Error(`${this.errorhead(right[0])} no lambda: expected "${snip}" to start with "."`); + } + const call = this.desugar_lambda(right[0].children, target); chunks = [[...left.slice(0, -1), call, ...right.slice(1)], ...rest]; // jazz (fast 2 hh) } else { //s jazz hh.fast 2 => (fast 2 (s jazz hh)) @@ -317,18 +367,15 @@ export class MondoRunner { console.log(printAst(ast)); return this.call(ast); } - errorhead(ast) { - return `[mondo ${ast.loc?.join(':') || ''}]`; - } call(ast, scope = []) { // for a node to be callable, it needs to be a list - this.assert(ast.type === 'list', `${this.errorhead(ast)} function call: expected list, got ${ast.type}`); + this.assert(ast.type === 'list', `${this.parser.errorhead(ast)} function call: expected list, got ${ast.type}`); // the first element is expected to be the function name const first = ast.children[0]; const name = first.value; this.assert( first?.type === 'plain', - `${this.errorhead(first)} expected function name, got ${first.type}${name ? ` "${name}"` : ''}.`, + `${this.parser.errorhead(first)} expected function name, got ${first.type}${name ? ` "${name}"` : ''}.`, ); if (name === 'lambda') { diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index a35eb8aa8..d5e33cbce 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -88,7 +88,7 @@ describe('mondo sugar', () => { it('should desugar . within , within []', () => expect(desguar('[bd cp . fast 2, x]')).toEqual('(stack (fast 2 (square bd cp)) x)')); - it('should desugar . ()', () => expect(desguar('[jazz hh.(fast 2)]')).toEqual('(square jazz (fast 2 hh))')); + it('should desugar .(.', () => expect(desguar('[jazz hh.(.fast 2)]')).toEqual('(square jazz (fast 2 hh))')); it('should desugar , |', () => expect(desguar('[bd, hh | oh]')).toEqual('(stack bd (or hh oh))')); it('should desugar , | of []', () => @@ -114,7 +114,15 @@ describe('mondo sugar', () => { it('should desugar x $ y . z', () => expect(desguar('x $ y . z')).toEqual('(z (x y))')); */ it('should desugar README example', () => - expect(desguar('s [bd hh*2 cp.(crush 4) ] . speed .8')).toEqual( + expect(desguar('s [bd hh*2 cp.(.crush 4) ] . speed .8')).toEqual( '(speed .8 (s (square bd (* 2 hh) (crush 4 cp) (angle mt ht lt))))', )); + + it('should desugar lambda', () => expect(desguar('(.fast 2)')).toEqual('(lambda (_) (fast 2 _))')); + it('should desugar lambda with pipe', () => + expect(desguar('(.fast 2 .room 1)')).toEqual('(lambda (_) (room 1 (fast 2 _)))')); + const lambda = parser.parse('(lambda (_) (fast 2 _))'); + const target = { type: 'plain', value: 'xyz' }; + it('should desugar_lambda', () => + expect(printAst(parser.desugar_lambda(lambda.children, target))).toEqual('(fast 2 xyz)')); }); From 7716fdb98e8ee64f70ef77472b7b38c6f7f3afd1 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 26 Mar 2025 03:22:23 +0100 Subject: [PATCH 058/174] mondo: refactor - rename MondoRunner.call => .evaluate - can now evaluate leafs - remove .( because it's confusing and half baked - support (.) as id function --- packages/mondo/README.md | 21 ++----- packages/mondo/mondo.mjs | 97 +++++++++--------------------- packages/mondo/test/mondo.test.mjs | 9 +-- 3 files changed, 37 insertions(+), 90 deletions(-) diff --git a/packages/mondo/README.md b/packages/mondo/README.md index 096d77d56..bd9104b9f 100644 --- a/packages/mondo/README.md +++ b/packages/mondo/README.md @@ -9,7 +9,7 @@ an experimental parser for an *uzulang*, a custom dsl for patterns that can stan import { MondoRunner } from 'uzu' const runner = MondoRunner({ seq, cat, s, crush, speed, '*': fast }); -const pat = runner.run('s [bd hh*2 cp.(.crush 4) ] . speed .8') +const pat = runner.run('s [bd hh*2 (cp.crush 4) ] . speed .8') ``` the above code will create the following call structure: @@ -52,16 +52,6 @@ n("0 1 2").add(n("<0 -4>")).scale("C:minor") --- -```plaintext -n[0 1 2].(add<0 -4>).scale"C minor" -``` - -```js -n("0 1 2".add("<0 -4>")).scale("C:minor") -``` - ---- - ```plaintext n[0 1 2].scale"C minor" .sometimes (12.note.add) @@ -75,11 +65,11 @@ n("0 1 2").scale("C:minor") --- ```plaintext -note g2*8.dec /2.(range .1 .4) +note g2*8.dec /2 ``` ```js -note("g2*8").dec(cat(sine, saw).slow(2).range(.1, .4)) +note("g2*8").dec(cat(sine, saw).range(.1, .4).slow(2)) ``` --- @@ -95,7 +85,7 @@ n("<0 1 2 3 4>*4").scale("C:minor").jux(cat(rev,press)) --- mondo` -sound [bd sd.(every 3 (.fast 4))].jux +sound [bd (sd.every 3 (.fast 4))].jux ` // og "Alternate Timelines for TidalCycles" example: // jux <(rev) (iter 4)> $ sound [bd (every 3 (fast 4) [sn])] @@ -129,9 +119,6 @@ comments variables: note g2*8.dec sine -sine.(range 0 4)/2 doesnt work -sine/2.(range 0 4) works - n (irand 8. ribbon 0 2) .scale"C minor" => lags because no whole ### reference diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 70015cc2d..165e597bb 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -176,31 +176,9 @@ export class MondoParser { } get_lambda(args, children) { // (.fast 2) = (lambda (_) (fast _ 2)) - const body = this.desugar(children); - return [ - { type: 'plain', value: 'lambda' }, - { type: 'list', children: args }, - { type: 'list', children: body }, - ]; - } - // inserts target into lambda body - desugar_lambda(lambda, target) { - // lambda looks like return value from this.get_lambda - const [_, args, body] = lambda; - if (args.length > 1) { - throw new Error('desugar_lambda with >1 arg is unsupported rn'); - } - const argNames = args.children.map((child) => child.value); - let desugar = (child) => { - if (child.type === 'plain' && child.value === argNames[0]) { - return target; - } - if (child.type === 'list') { - child.children = child.children.map(desugar); - } - return child; - }; - return desugar(body); + children = this.desugar(children); + const body = children.length === 1 ? children[0] : { type: 'list', children }; + return [{ type: 'plain', value: 'lambda' }, { type: 'list', children: args }, body]; } // returns location range of given ast (even if desugared) get_range(ast, range = [Infinity, 0]) { @@ -228,40 +206,23 @@ export class MondoParser { let chunks = this.split_children(children, 'pipe'); while (chunks.length > 1) { let [left, right, ...rest] = chunks; + + if (right.length && right[0].type === 'list') { + // x.(y) => not allowed anymore for now.. + const snip = this.get_code_snippet(right[0]); + throw new Error(`${this.errorhead(right[0])} cannot apply list: expected "(${snip})" to be a word`); + } if (!left.length) { const arg = { type: 'plain', value: '_' }; return this.get_lambda([arg], [arg, ...children]); } - if (right.length && right[0].type === 'list') { - // s jazz hh.(.fast 2) => s jazz (hh.fast 2) = s jazz (fast 2 hh) - const target = left[left.length - 1]; // hh - - if (right[0].children[0].value !== 'lambda') { - const snip = this.get_code_snippet(right[0]); - throw new Error(`${this.errorhead(right[0])} no lambda: expected "${snip}" to start with "."`); - } - const call = this.desugar_lambda(right[0].children, target); - chunks = [[...left.slice(0, -1), call, ...right.slice(1)], ...rest]; // jazz (fast 2 hh) - } else { - //s jazz hh.fast 2 => (fast 2 (s jazz hh)) - // const call = left.length > 1 ? { type: 'list', children: next(left) } : left[0]; - const call = left.length > 1 ? { type: 'list', children: left } : left[0]; - chunks = [[...right, call], ...rest]; - } + // s jazz hh.fast 2 => (fast 2 (s jazz hh)) + const call = left.length > 1 ? { type: 'list', children: left } : left[0]; + chunks = [[...right, call], ...rest]; } // return next(chunks[0]); return chunks[0]; } - /* desugar_dollars(children) { - let chunks = this.split_children(children, 'dollar'); - while (chunks.length > 1) { - let [left, right, ...rest] = chunks; - //fast 2 $ s jazz hh => (fast 2 (s jazz hh)) - const call = right.length > 1 ? { type: 'list', children: right } : right[0]; - chunks = [[...left, call], ...rest]; - } - return chunks[0]; - } */ parse_pair(open_type, close_type) { this.consume(open_type); const children = []; @@ -365,11 +326,20 @@ export class MondoRunner { run(code, offset = 0) { const ast = this.parser.parse(code, offset); console.log(printAst(ast)); - return this.call(ast); + return this.evaluate(ast); } - call(ast, scope = []) { - // for a node to be callable, it needs to be a list - this.assert(ast.type === 'list', `${this.parser.errorhead(ast)} function call: expected list, got ${ast.type}`); + evaluate(ast, scope = []) { + if (ast.type !== 'list') { + // is leaf + if (ast.type === 'number') { + ast.value = Number(ast.value); + } else if (['quotes_double', 'quotes_single'].includes(ast.type)) { + arg.value = arg.value.slice(1, -1); + } + return this.lib.leaf(ast, scope); + } + + // is list // the first element is expected to be the function name const first = ast.children[0]; const name = first.value; @@ -385,23 +355,12 @@ export class MondoRunner { scope = { [argNames[0]]: x, // TODO: merge scope... + support multiple args }; - return this.call(body, scope); + return this.evaluate(body, scope); }; } - // process args - const args = ast.children.slice(1).map((arg) => { - if (arg.type === 'list') { - return this.call(arg, scope); - } - if (arg.type === 'number') { - arg.value = Number(arg.value); - } else if (['quotes_double', 'quotes_single'].includes(arg.type)) { - arg.value = arg.value.slice(1, -1); - } - return this.lib.leaf(arg, scope); - }); - + // evaluate args + const args = ast.children.slice(1).map((arg) => this.evaluate(arg, scope)); return this.lib.call(name, args, scope); } } diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index d5e33cbce..2fd45d46d 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -88,7 +88,7 @@ describe('mondo sugar', () => { it('should desugar . within , within []', () => expect(desguar('[bd cp . fast 2, x]')).toEqual('(stack (fast 2 (square bd cp)) x)')); - it('should desugar .(.', () => expect(desguar('[jazz hh.(.fast 2)]')).toEqual('(square jazz (fast 2 hh))')); + // it('should desugar .(.', () => expect(desguar('[jazz hh.(.fast 2)]')).toEqual('(square jazz (fast 2 hh))')); it('should desugar , |', () => expect(desguar('[bd, hh | oh]')).toEqual('(stack bd (or hh oh))')); it('should desugar , | of []', () => @@ -114,15 +114,16 @@ describe('mondo sugar', () => { it('should desugar x $ y . z', () => expect(desguar('x $ y . z')).toEqual('(z (x y))')); */ it('should desugar README example', () => - expect(desguar('s [bd hh*2 cp.(.crush 4) ] . speed .8')).toEqual( + expect(desguar('s [bd hh*2 (cp.crush 4) ] . speed .8')).toEqual( '(speed .8 (s (square bd (* 2 hh) (crush 4 cp) (angle mt ht lt))))', )); + it('should desugar (.)', () => expect(desguar('(.)')).toEqual('(lambda (_) _)')); it('should desugar lambda', () => expect(desguar('(.fast 2)')).toEqual('(lambda (_) (fast 2 _))')); it('should desugar lambda with pipe', () => expect(desguar('(.fast 2 .room 1)')).toEqual('(lambda (_) (room 1 (fast 2 _)))')); - const lambda = parser.parse('(lambda (_) (fast 2 _))'); + /* const lambda = parser.parse('(lambda (_) (fast 2 _))'); const target = { type: 'plain', value: 'xyz' }; it('should desugar_lambda', () => - expect(printAst(parser.desugar_lambda(lambda.children, target))).toEqual('(fast 2 xyz)')); + expect(printAst(parser.desugar_lambda(lambda.children, target))).toEqual('(fast 2 xyz)')); */ }); From abc7a1e48d3e8cfb301d39a711ddb59ffcf8ae38 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 26 Mar 2025 03:55:00 +0100 Subject: [PATCH 059/174] mondo: this is the way --- packages/mondo/mondo.mjs | 17 +++++++---------- packages/mondough/mondough.mjs | 5 +++++ 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 165e597bb..3ff781256 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -207,11 +207,6 @@ export class MondoParser { while (chunks.length > 1) { let [left, right, ...rest] = chunks; - if (right.length && right[0].type === 'list') { - // x.(y) => not allowed anymore for now.. - const snip = this.get_code_snippet(right[0]); - throw new Error(`${this.errorhead(right[0])} cannot apply list: expected "(${snip})" to be a word`); - } if (!left.length) { const arg = { type: 'plain', value: '_' }; return this.get_lambda([arg], [arg, ...children]); @@ -342,11 +337,13 @@ export class MondoRunner { // is list // the first element is expected to be the function name const first = ast.children[0]; - const name = first.value; - this.assert( - first?.type === 'plain', - `${this.parser.errorhead(first)} expected function name, got ${first.type}${name ? ` "${name}"` : ''}.`, - ); + let name; + if (first?.type !== 'list') { + name = first.value; // regular function call e.g. (fast 2 (s bd)) + } else { + // dynamic function name e.g. "( 2 (s bd))" + name = this.evaluate(first); + } if (name === 'lambda') { const [_, args, body] = ast.children; diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index f42f0ca76..081d054b9 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -11,6 +11,7 @@ import { chooseIn, degradeBy, silence, + isPattern, } from '@strudel/core'; import { registerLanguage } from '@strudel/transpiler'; import { MondoRunner } from '../mondo/mondo.mjs'; @@ -42,6 +43,10 @@ lib['or'] = (...children) => chooseIn(...children); // always has structure but let runner = new MondoRunner({ call(name, args, scope) { + if (isPattern(name)) { + // patterned function name, e.g. "s bd . 2" + return name.fmap((fn) => fn(...args)).innerJoin(); + } const fn = lib[name] || strudelScope[name]; if (!fn) { throw new Error(`[moundough]: unknown function "${name}"`); From 1738e4d53892d0434082b4b3af0b957530fa804b Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 26 Mar 2025 03:55:54 +0100 Subject: [PATCH 060/174] fix: lint --- packages/mondo/mondo.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 3ff781256..5419d15b5 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -329,7 +329,7 @@ export class MondoRunner { if (ast.type === 'number') { ast.value = Number(ast.value); } else if (['quotes_double', 'quotes_single'].includes(ast.type)) { - arg.value = arg.value.slice(1, -1); + ast.value = ast.value.slice(1, -1); } return this.lib.leaf(ast, scope); } From 64a6dacc1ed8975ff99c7022aac1495db412fb79 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 26 Mar 2025 09:27:02 +0100 Subject: [PATCH 061/174] mondo: patternable function names --- packages/mondo/mondo.mjs | 19 ++++------- packages/mondough/mondough.mjs | 37 +++++++++++++++------- website/src/pages/learn/mondo-notation.mdx | 4 +-- 3 files changed, 34 insertions(+), 26 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 5419d15b5..d031af4eb 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -280,7 +280,7 @@ export class MondoParser { get_locations(code, offset = 0) { let walk = (ast, locations = []) => { if (ast.type === 'list') { - return ast.children.slice(1).forEach((child) => walk(child, locations)); + return ast.children.forEach((child) => walk(child, locations)); } if (ast.loc) { locations.push(ast.loc); @@ -335,17 +335,11 @@ export class MondoRunner { } // is list - // the first element is expected to be the function name - const first = ast.children[0]; - let name; - if (first?.type !== 'list') { - name = first.value; // regular function call e.g. (fast 2 (s bd)) - } else { - // dynamic function name e.g. "( 2 (s bd))" - name = this.evaluate(first); + if (!ast.children.length) { + throw new Error(`empty list`); } - if (name === 'lambda') { + if (ast.children[0].value === 'lambda') { const [_, args, body] = ast.children; const argNames = args.children.map((child) => child.value); return (x) => { @@ -356,8 +350,9 @@ export class MondoRunner { }; } + const args = ast.children.map((arg) => this.evaluate(arg, scope)); + // we could short circuit arg[0] if its plain... // evaluate args - const args = ast.children.slice(1).map((arg) => this.evaluate(arg, scope)); - return this.lib.call(name, args, scope); + return this.lib.call(args[0], args.slice(1), scope); } } diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index 081d054b9..01247e78b 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -11,7 +11,6 @@ import { chooseIn, degradeBy, silence, - isPattern, } from '@strudel/core'; import { registerLanguage } from '@strudel/transpiler'; import { MondoRunner } from '../mondo/mondo.mjs'; @@ -24,7 +23,10 @@ const arrayRange = (start, stop, step = 1) => ); const range = (max, min) => min.squeezeBind((a) => max.bind((b) => seq(...arrayRange(a, b)))); +let nope = (...args) => args[args.length - 1]; + let lib = {}; +lib['nope'] = nope; lib['-'] = silence; lib['~'] = silence; lib.curly = stepcat; @@ -43,15 +45,19 @@ lib['or'] = (...children) => chooseIn(...children); // always has structure but let runner = new MondoRunner({ call(name, args, scope) { - if (isPattern(name)) { - // patterned function name, e.g. "s bd . 2" - return name.fmap((fn) => fn(...args)).innerJoin(); + // name is expected to be a pattern of functions! + const first = name.firstCycle(true)[0]; + if (typeof first?.value !== 'function') { + throw new Error(`[mondough] "${first}" is not a function`); } - const fn = lib[name] || strudelScope[name]; - if (!fn) { - throw new Error(`[moundough]: unknown function "${name}"`); - } - return fn(...args); + return name + .fmap((fn) => { + if (typeof fn !== 'function') { + throw new Error(`[mondough] "${fn}" is not a function`); + } + return fn(...args); + }) + .innerJoin(); }, leaf(token, scope) { let { value, type } = token; @@ -59,14 +65,21 @@ let runner = new MondoRunner({ if (type === 'plain' && scope[value]) { return reify(scope[value]); // -> local scope has no location } - const [from, to] = token.loc; const variable = lib[value] ?? strudelScope[value]; + let pat; if (type === 'plain' && typeof variable !== 'undefined') { // problem: collisions when we want a string that happens to also be a variable name // example: "s sine" -> sine is also a variable - return reify(strudelScope[value]).withLoc(from, to); + pat = reify(variable); + } else { + pat = reify(value); } - return reify(value).withLoc(from, to); + + if (token.loc) { + pat = pat.withLoc(token.loc[0], token.loc[1]); + } + pat.foo = true; + return pat; }, }); diff --git a/website/src/pages/learn/mondo-notation.mdx b/website/src/pages/learn/mondo-notation.mdx index aa7904987..2bc952341 100644 --- a/website/src/pages/learn/mondo-notation.mdx +++ b/website/src/pages/learn/mondo-notation.mdx @@ -14,7 +14,7 @@ Here's an example: <8 16>) . *2 + tune={`$ note (c2 .euclid <3 6 3> <8 16>) . *2 .s "sine" .add (note [0 <12 24>]*2) .dec(sine .range .2 2) .room .5 .lpf(sine/3.range 120 400) @@ -27,7 +27,7 @@ $ s [bd bd bd bd] .bank tr909.clip.5 .ply<1 [1 [2 4]]> $ s oh*4 .press .bank tr909 .speed.8 -.dec <.02 .05>*2 .(add (saw/8.range 0 1))`} +.dec (<.02 .05>*2 .add (saw/8.range 0 1))`} /> ## Mondo in the REPL From 6b2b8b5124c9eefa1d25904958a09871b1e2cb55 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 26 Mar 2025 09:30:49 +0100 Subject: [PATCH 062/174] fix: test --- packages/mondo/test/mondo.test.mjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index 2fd45d46d..f987cbc2b 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -12,7 +12,7 @@ const p = (code) => parser.parse(code, -1); describe('mondo tokenizer', () => { const parser = new MondoParser(); - it('should tokenize with loangleions', () => + it('should tokenize with locations', () => expect( parser .tokenize('(one two three)') @@ -22,6 +22,7 @@ describe('mondo tokenizer', () => { // it('should parse with loangleions', () => expect(parser.parse('(one two three)')).toEqual()); it('should get loangleions', () => expect(parser.get_locations('s bd rim')).toEqual([ + [0, 1], [2, 4], [5, 8], ])); From e9de45993e2c9fe9fa002ccf2af361fc8dea6214 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 29 Mar 2025 19:54:04 +0100 Subject: [PATCH 063/174] signal: remove signal synonyms for now --- packages/core/signal.mjs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index 64a177253..9152b6a32 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -72,27 +72,23 @@ export const sine2 = signal((t) => Math.sin(Math.PI * 2 * t)); /** * A sine signal between 0 and 1. * @return {Pattern} - * @synonyms sin * @example * n(sine.segment(16).range(0,15)) * .scale("C:minor") * */ export const sine = sine2.fromBipolar(); -export const sin = sine; /** * A cosine signal between 0 and 1. * * @return {Pattern} - * @synonyms cos * @example * n(stack(sine,cosine).segment(16).range(0,15)) * .scale("C:minor") * */ export const cosine = sine._early(Fraction(1).div(4)); -export const cos = cosine; /** * A cosine signal between -1 and 1 (like `cosine`, but bipolar). @@ -105,13 +101,11 @@ export const cosine2 = sine2._early(Fraction(1).div(4)); * A square signal between 0 and 1. * * @return {Pattern} - * @synonyms sqr * @example * n(square.segment(4).range(0,7)).scale("C:minor") * */ export const square = signal((t) => Math.floor((t * 2) % 2)); -export const sqr = square; /** * A square signal between -1 and 1 (like `square`, but bipolar). From c9f58220a3c8bf02a35559ac9489d00becb29c2e Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 29 Mar 2025 19:55:29 +0100 Subject: [PATCH 064/174] remove minor change --- packages/core/signal.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index 9152b6a32..ac8aa3e3d 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -99,7 +99,6 @@ export const cosine2 = sine2._early(Fraction(1).div(4)); /** * A square signal between 0 and 1. - * * @return {Pattern} * @example * n(square.segment(4).range(0,7)).scale("C:minor") From 2e017e46f94962b2ec43a8db44853bcdd4d6ddc0 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 29 Mar 2025 20:17:09 +0100 Subject: [PATCH 065/174] trim readme --- packages/mondo/README.md | 124 --------------------------------------- 1 file changed, 124 deletions(-) diff --git a/packages/mondo/README.md b/packages/mondo/README.md index bd9104b9f..551690467 100644 --- a/packages/mondo/README.md +++ b/packages/mondo/README.md @@ -25,127 +25,3 @@ the above code will create the following call structure: ) .8 ) ``` - -you can pass all available functions to *MondoRunner* as an object. - -## snippets / thoughts - -### variants of add - -```plaintext -n[0 1 2].add(<0 -4>.n).scale"C minor" -``` - -```js -n("0 1 2").add("<0 -4>".n()).scale("C:minor") -``` - ---- - -```plaintext -n[0 1 2].add(n<0 -4>).scale"C minor" -``` - -```js -n("0 1 2").add(n("<0 -4>")).scale("C:minor") -``` - ---- - -```plaintext -n[0 1 2].scale"C minor" -.sometimes (12.note.add) -``` - -```js -n("0 1 2").scale("C:minor") -.sometimes(add(note("12"))) -``` - ---- - -```plaintext -note g2*8.dec /2 -``` - -```js -note("g2*8").dec(cat(sine, saw).range(.1, .4).slow(2)) -``` - ---- - -```plaintext -n <0 1 2 3 4>*4 .scale"C minor" .jux -``` - -```js -n("<0 1 2 3 4>*4").scale("C:minor").jux(cat(rev,press)) -``` - ---- - -mondo` -sound [bd (sd.every 3 (.fast 4))].jux -` -// og "Alternate Timelines for TidalCycles" example: -// jux <(rev) (iter 4)> $ sound [bd (every 3 (fast 4) [sn])] - -### things mondo cant do - -how to write lists? - -```js -arrange( - [4, "(3,8)"], - [2, "(5,8)"] -).note() -``` - -how to write objects: - -```js -samples({ rave: 'rave/AREUREADY.wav' }, 'github:tidalcycles/dirt-samples') -``` - -how to access array indices? - -```js -note("<[c,eb,g]!2 [c,f,ab] [d,f,ab]>").arpWith(haps => haps[2]) -``` - -s hh .struct(binaryN 55532 16) - -comments - -variables: note g2*8.dec sine - -n (irand 8. ribbon 0 2) .scale"C minor" => lags because no whole - -### reference - -- arp: note <[c,eb,g] [c,f,ab] [d,f,ab]> .arp [0 [0,2] 1 [0,2]] -- bank: s [bd sd [- bd] sd].bank TR909 -- beat: s sd .beat [4,12] 16 -- binary: s hh .struct (binary 5) -- binaryN: s hh .struct(binaryN 55532 16) => is wrong -- bite: n[0 1 2 3 4 5 6 7].scale"c mixolydian".bite 4 [3 2 1 0] -- bpattack: note [c2 e2 f2 g2].s sawtooth.bpf 500.bpa <.5 .25 .1 .01>/4.bpenv 4 - -### dot is a bit ambiguous - -```plaintext -n[0 1 2].scale"C minor".ad.1 -``` - -decimal vs pipe - -### less ambiguity with [] and "" - -in js, s("hh cp") implcitily does [hh cp] -in mondo, s[hh cp] always shows the type of bracket used - -### todo - -- lists: C:minor -- spread: [0 .. 2] -- replicate: ! From c9cafa37fdb521d87d4a690d68e8092b841bf209 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 29 Mar 2025 21:33:51 +0100 Subject: [PATCH 066/174] mondo: improve mondo package api + update readme --- packages/mondo/README.md | 45 +++++++++++++++---------- packages/mondo/mondo.mjs | 24 +++++--------- packages/mondo/test/mondo.test.mjs | 27 ++++++++++++++- packages/mondough/mondough.mjs | 53 ++++++++++++++++-------------- 4 files changed, 89 insertions(+), 60 deletions(-) diff --git a/packages/mondo/README.md b/packages/mondo/README.md index 551690467..47812e83d 100644 --- a/packages/mondo/README.md +++ b/packages/mondo/README.md @@ -5,23 +5,32 @@ an experimental parser for an *uzulang*, a custom dsl for patterns that can stan - [uzulang I](https://garten.salat.dev/uzu/uzulang1.html) - [uzulang II](https://garten.salat.dev/uzu/uzulang2.html) +## Example Usage + ```js -import { MondoRunner } from 'uzu' - -const runner = MondoRunner({ seq, cat, s, crush, speed, '*': fast }); -const pat = runner.run('s [bd hh*2 (cp.crush 4) ] . speed .8') -``` - -the above code will create the following call structure: - -```lisp -(speed - (s - (seq bd - (* hh 2) - (crush cp 4) - (cat mt ht lt) - ) - ) .8 -) +import { MondoRunner } from 'mondo' +// define our library of functions and variables +let lib = { + add: (a, b) => a + b, + mul: (a, b) => a * b, + PI: Math.PI, +}; +// this function will evaluate nodes in the syntax tree +function evaluator(node) { + // check if node is a leaf node (!= list) + if (node.type !== 'list') { + // check lib if we find a match in the lib, otherwise return value + return lib[node.value] ?? node.value; + } + // now it can only be a list.. + const [fn, ...args] = node.children; + // children in a list will already be evaluated + // the first child is expected to be a function + if (typeof fn !== 'function') { + throw new Error(`"${fn}" is not a function ${typeof fn}`); + } + return fn(...args); +} +const runner = new MondoRunner(evaluator); +const pat = runner.run('add 1 (mul 2 PI)') // 7.283185307179586 ``` diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index d031af4eb..673841cde 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -306,11 +306,10 @@ export function printAst(ast, compact = false, lvl = 0) { // lisp runner export class MondoRunner { - constructor(lib) { + constructor(evaluator) { this.parser = new MondoParser(); - this.lib = lib; - this.assert(!!this.lib.leaf, `no handler for leaft nodes! add "leaf" to your lib`); - this.assert(!!this.lib.call, `no handler for call nodes! add "call" to your lib`); + this.evaluator = evaluator; + this.assert(typeof evaluator === 'function', `expected an evaluator function to be passed to new MondoRunner`); } // a helper to check conditions and throw if they are not met assert(condition, error) { @@ -331,15 +330,10 @@ export class MondoRunner { } else if (['quotes_double', 'quotes_single'].includes(ast.type)) { ast.value = ast.value.slice(1, -1); } - return this.lib.leaf(ast, scope); + return this.evaluator(ast, scope); } - // is list - if (!ast.children.length) { - throw new Error(`empty list`); - } - - if (ast.children[0].value === 'lambda') { + if (ast.children[0]?.value === 'lambda') { const [_, args, body] = ast.children; const argNames = args.children.map((child) => child.value); return (x) => { @@ -349,10 +343,8 @@ export class MondoRunner { return this.evaluate(body, scope); }; } - - const args = ast.children.map((arg) => this.evaluate(arg, scope)); - // we could short circuit arg[0] if its plain... - // evaluate args - return this.lib.call(args[0], args.slice(1), scope); + // evaluate all children before evaluating list + ast.children = ast.children.map((arg) => this.evaluate(arg, scope)); + return this.evaluator(ast, scope); } } diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index f987cbc2b..338a1fec8 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th */ import { describe, expect, it } from 'vitest'; -import { MondoParser, printAst } from '../mondo.mjs'; +import { MondoParser, printAst, MondoRunner } from '../mondo.mjs'; const parser = new MondoParser(); const p = (code) => parser.parse(code, -1); @@ -128,3 +128,28 @@ describe('mondo sugar', () => { it('should desugar_lambda', () => expect(printAst(parser.desugar_lambda(lambda.children, target))).toEqual('(fast 2 xyz)')); */ }); + +describe('mondo arithmetic', () => { + let lib = { + add: (a, b) => a + b, + mul: (a, b) => a * b, + PI: Math.PI, + }; + function evaluator(node) { + // check if node is a leaf node (!= list) + if (node.type !== 'list') { + // check lib if we find a match in the lib, otherwise return value + return lib[node.value] ?? node.value; + } + // now it can only be a list.. + const [fn, ...args] = node.children; + // children in a list will already be evaluated + // the first child is expected to be a function + if (typeof fn !== 'function') { + throw new Error(`"${fn}" is not a function ${typeof fn}`); + } + return fn(...args); + } + const runner = new MondoRunner(evaluator); + it('should desugar (.)', () => expect(runner.run('add 1 (mul 2 PI)').toFixed(2)).toEqual('7.28')); +}); diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index 01247e78b..d84ceb0cf 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -43,8 +43,12 @@ lib['..'] = range; lib['or'] = (...children) => chooseIn(...children); // always has structure but is cyclewise.. e.g. "s oh*8.dec[.04 | .5]" //lib['or'] = (...children) => chooseOut(...children); // "s oh*8.dec[.04 | .5]" is better but "dec[.04 | .5].s oh*8" has no struct -let runner = new MondoRunner({ - call(name, args, scope) { +function evaluator(node, scope) { + const { type } = node; + // node is list + if (type === 'list') { + const { children } = node; + const [name, ...args] = children; // name is expected to be a pattern of functions! const first = name.firstCycle(true)[0]; if (typeof first?.value !== 'function') { @@ -58,30 +62,29 @@ let runner = new MondoRunner({ return fn(...args); }) .innerJoin(); - }, - leaf(token, scope) { - let { value, type } = token; - // local scope - if (type === 'plain' && scope[value]) { - return reify(scope[value]); // -> local scope has no location - } - const variable = lib[value] ?? strudelScope[value]; - let pat; - if (type === 'plain' && typeof variable !== 'undefined') { - // problem: collisions when we want a string that happens to also be a variable name - // example: "s sine" -> sine is also a variable - pat = reify(variable); - } else { - pat = reify(value); - } + } + // node is leaf + let { value } = node; + if (type === 'plain' && scope[value]) { + return reify(scope[value]); // -> local scope has no location + } + const variable = lib[value] ?? strudelScope[value]; + let pat; + if (type === 'plain' && typeof variable !== 'undefined') { + // problem: collisions when we want a string that happens to also be a variable name + // example: "s sine" -> sine is also a variable + pat = reify(variable); + } else { + pat = reify(value); + } + if (node.loc) { + pat = pat.withLoc(node.loc[0], node.loc[1]); + } + pat.foo = true; + return pat; +} - if (token.loc) { - pat = pat.withLoc(token.loc[0], token.loc[1]); - } - pat.foo = true; - return pat; - }, -}); +let runner = new MondoRunner(evaluator); export function mondo(code, offset = 0) { if (Array.isArray(code)) { From b4027fd92ac38f9426e29982bd444fa197754f33 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 30 Mar 2025 11:36:50 +0200 Subject: [PATCH 067/174] fix: mondo dont mutate (breaks lambdas) --- packages/mondo/mondo.mjs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 673841cde..0e8dc8576 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -343,8 +343,9 @@ export class MondoRunner { return this.evaluate(body, scope); }; } - // evaluate all children before evaluating list - ast.children = ast.children.map((arg) => this.evaluate(arg, scope)); - return this.evaluator(ast, scope); + // evaluate all children before evaluating list (dont mutate!!!) + const args = ast.children.map((arg) => this.evaluate(arg, scope)); + const node = { type: 'list', children: args }; + return this.evaluator(node, scope); } } From a0fb8fb37f36644e5e344a938e828ec22ffb0cd0 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 30 Mar 2025 11:43:01 +0200 Subject: [PATCH 068/174] mondo: def node --- packages/mondo/mondo.mjs | 21 +++++++++++++++++---- packages/mondo/test/mondo.test.mjs | 6 +++--- packages/mondough/mondough.mjs | 11 +++++++---- 3 files changed, 27 insertions(+), 11 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 0e8dc8576..89236a3bb 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -175,10 +175,10 @@ export class MondoParser { return children; } get_lambda(args, children) { - // (.fast 2) = (lambda (_) (fast _ 2)) + // (.fast 2) = (fn (_) (fast _ 2)) children = this.desugar(children); const body = children.length === 1 ? children[0] : { type: 'list', children }; - return [{ type: 'plain', value: 'lambda' }, { type: 'list', children: args }, body]; + return [{ type: 'plain', value: 'fn' }, { type: 'list', children: args }, body]; } // returns location range of given ast (even if desugared) get_range(ast, range = [Infinity, 0]) { @@ -322,7 +322,7 @@ export class MondoRunner { console.log(printAst(ast)); return this.evaluate(ast); } - evaluate(ast, scope = []) { + evaluate(ast, scope = {}) { if (ast.type !== 'list') { // is leaf if (ast.type === 'number') { @@ -333,7 +333,20 @@ export class MondoRunner { return this.evaluator(ast, scope); } - if (ast.children[0]?.value === 'lambda') { + if (ast.children[0]?.value === 'def') { + if (ast.children.length !== 3) { + throw new Error(`expected "def" to have 3 children, but got ${ast.children.length}`); + } + // (def myfn (fn (_) (ply 2 _))) + // ^name ^body + const name = ast.children[1].value; + const body = this.evaluate(ast.children[2], scope); + scope[name] = body; + return this.evaluator(ast, scope); + } + if (ast.children[0]?.value === 'fn') { + // (fn (_) (ply 2 _) + // ^args ^ body const [_, args, body] = ast.children; const argNames = args.children.map((child) => child.value); return (x) => { diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index 338a1fec8..47e0e165d 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -119,10 +119,10 @@ describe('mondo sugar', () => { '(speed .8 (s (square bd (* 2 hh) (crush 4 cp) (angle mt ht lt))))', )); - it('should desugar (.)', () => expect(desguar('(.)')).toEqual('(lambda (_) _)')); - it('should desugar lambda', () => expect(desguar('(.fast 2)')).toEqual('(lambda (_) (fast 2 _))')); + it('should desugar (.)', () => expect(desguar('(.)')).toEqual('(fn (_) _)')); + it('should desugar lambda', () => expect(desguar('(.fast 2)')).toEqual('(fn (_) (fast 2 _))')); it('should desugar lambda with pipe', () => - expect(desguar('(.fast 2 .room 1)')).toEqual('(lambda (_) (room 1 (fast 2 _)))')); + expect(desguar('(.fast 2 .room 1)')).toEqual('(fn (_) (room 1 (fast 2 _)))')); /* const lambda = parser.parse('(lambda (_) (fast 2 _))'); const target = { type: 'plain', value: 'xyz' }; it('should desugar_lambda', () => diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index d84ceb0cf..543dcfeb2 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -49,15 +49,19 @@ function evaluator(node, scope) { if (type === 'list') { const { children } = node; const [name, ...args] = children; + if (name.value === 'def') { + return silence; + } // name is expected to be a pattern of functions! const first = name.firstCycle(true)[0]; - if (typeof first?.value !== 'function') { - throw new Error(`[mondough] "${first}" is not a function`); + const type = typeof first?.value; + if (type !== 'function') { + throw new Error(`[mondough] "${first}" is not a function, got ${type} ...`); } return name .fmap((fn) => { if (typeof fn !== 'function') { - throw new Error(`[mondough] "${fn}" is not a function`); + throw new Error(`[mondough] "${fn}" is not a function b`); } return fn(...args); }) @@ -80,7 +84,6 @@ function evaluator(node, scope) { if (node.loc) { pat = pat.withLoc(node.loc[0], node.loc[1]); } - pat.foo = true; return pat; } From f2372e7a415da0edce6d9460327d652a487e18ba Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 30 Mar 2025 17:55:37 +0200 Subject: [PATCH 069/174] mondo: refactor evaluate function into bits --- packages/mondo/README.md | 2 +- packages/mondo/mondo.mjs | 75 +++++++++++++++++------------- packages/mondo/test/mondo.test.mjs | 2 +- packages/mondough/mondough.mjs | 2 +- 4 files changed, 45 insertions(+), 36 deletions(-) diff --git a/packages/mondo/README.md b/packages/mondo/README.md index 47812e83d..d283ccf31 100644 --- a/packages/mondo/README.md +++ b/packages/mondo/README.md @@ -31,6 +31,6 @@ function evaluator(node) { } return fn(...args); } -const runner = new MondoRunner(evaluator); +const runner = new MondoRunner({ evaluator }); const pat = runner.run('add 1 (mul 2 PI)') // 7.283185307179586 ``` diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 89236a3bb..e19116ae6 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -306,7 +306,7 @@ export function printAst(ast, compact = false, lvl = 0) { // lisp runner export class MondoRunner { - constructor(evaluator) { + constructor({ evaluator } = {}) { this.parser = new MondoParser(); this.evaluator = evaluator; this.assert(typeof evaluator === 'function', `expected an evaluator function to be passed to new MondoRunner`); @@ -322,43 +322,52 @@ export class MondoRunner { console.log(printAst(ast)); return this.evaluate(ast); } - evaluate(ast, scope = {}) { - if (ast.type !== 'list') { - // is leaf - if (ast.type === 'number') { - ast.value = Number(ast.value); - } else if (['quotes_double', 'quotes_single'].includes(ast.type)) { - ast.value = ast.value.slice(1, -1); - } - return this.evaluator(ast, scope); + evaluate_def(ast, scope) { + // (def name body) + if (ast.children.length !== 3) { + throw new Error(`expected "def" to have 3 children, but got ${ast.children.length}`); } - - if (ast.children[0]?.value === 'def') { - if (ast.children.length !== 3) { - throw new Error(`expected "def" to have 3 children, but got ${ast.children.length}`); - } - // (def myfn (fn (_) (ply 2 _))) - // ^name ^body - const name = ast.children[1].value; - const body = this.evaluate(ast.children[2], scope); - scope[name] = body; - return this.evaluator(ast, scope); - } - if (ast.children[0]?.value === 'fn') { - // (fn (_) (ply 2 _) - // ^args ^ body - const [_, args, body] = ast.children; - const argNames = args.children.map((child) => child.value); - return (x) => { - scope = { - [argNames[0]]: x, // TODO: merge scope... + support multiple args - }; - return this.evaluate(body, scope); + const name = ast.children[1].value; + const body = this.evaluate(ast.children[2], scope); + scope[name] = body; + return this.evaluator(ast, scope); + } + evaluate_lambda(ast, scope) { + // (fn (_) (ply 2 _) + // ^args ^ body + const [_, args, body] = ast.children; + const argNames = args.children.map((child) => child.value); + return (x) => { + scope = { + [argNames[0]]: x, // TODO: merge scope... + support multiple args }; - } + return this.evaluate(body, scope); + }; + } + evaluate_list(ast, scope) { // evaluate all children before evaluating list (dont mutate!!!) const args = ast.children.map((arg) => this.evaluate(arg, scope)); const node = { type: 'list', children: args }; return this.evaluator(node, scope); } + evaluate_leaf(ast, scope) { + if (ast.type === 'number') { + ast.value = Number(ast.value); + } else if (['quotes_double', 'quotes_single'].includes(ast.type)) { + ast.value = ast.value.slice(1, -1); + } + return this.evaluator(ast, scope); + } + evaluate(ast, scope = {}) { + if (ast.type !== 'list') { + return this.evaluate_leaf(ast, scope); + } + if (ast.children[0]?.value === 'def') { + return this.evaluate_def(ast, scope); + } + if (ast.children[0]?.value === 'fn') { + return this.evaluate_lambda(ast, scope); + } + return this.evaluate_list(ast, scope); + } } diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index 47e0e165d..6f8090cc5 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -150,6 +150,6 @@ describe('mondo arithmetic', () => { } return fn(...args); } - const runner = new MondoRunner(evaluator); + const runner = new MondoRunner({ evaluator }); it('should desugar (.)', () => expect(runner.run('add 1 (mul 2 PI)').toFixed(2)).toEqual('7.28')); }); diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index 543dcfeb2..effaf367c 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -87,7 +87,7 @@ function evaluator(node, scope) { return pat; } -let runner = new MondoRunner(evaluator); +let runner = new MondoRunner({ evaluator }); export function mondo(code, offset = 0) { if (Array.isArray(code)) { From 94a3a60e4917e7c5592aea35b1ded71e6005ad60 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Tue, 1 Apr 2025 21:49:28 +0200 Subject: [PATCH 070/174] mondo: remember pair locations + allow passing a scope to MondoRunner.run + make def a side effect + proper lambda with multiple args + closure --- packages/mondo/README.md | 2 +- packages/mondo/mondo.mjs | 65 +++++++++++++++++++--------------- packages/mondough/mondough.mjs | 2 +- 3 files changed, 38 insertions(+), 31 deletions(-) diff --git a/packages/mondo/README.md b/packages/mondo/README.md index d283ccf31..8bdd93143 100644 --- a/packages/mondo/README.md +++ b/packages/mondo/README.md @@ -27,7 +27,7 @@ function evaluator(node) { // children in a list will already be evaluated // the first child is expected to be a function if (typeof fn !== 'function') { - throw new Error(`"${fn}" is not a function ${typeof fn}`); + throw new Error(`"${fn}" is not a function`); } return fn(...args); } diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index e19116ae6..2553f74fb 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -24,7 +24,7 @@ export class MondoParser { pipe: /^\./, stack: /^[,$]/, or: /^[|]/, - plain: /^[a-zA-Z0-9-~_^#]+/, + plain: /^[a-zA-Z0-9-~_^#+-]+/, }; // matches next token next_token(code, offset = 0) { @@ -88,6 +88,8 @@ export class MondoParser { parse_expr() { if (!this.tokens[0]) { throw new Error(`unexpected end of file`); + // TODO: could we allow that? like (((((((( s bd + // return { type: 'list', children: [] }; } let next = this.tokens[0]?.type; if (next === 'open_list') { @@ -219,13 +221,17 @@ export class MondoParser { return chunks[0]; } parse_pair(open_type, close_type) { + const begin = this.tokens[0].loc?.[0]; this.consume(open_type); const children = []; while (this.tokens[0]?.type !== close_type) { children.push(this.parse_expr()); } + const end = this.tokens[0].loc?.[1]; this.consume(close_type); - return children; + const node = { type: 'list', children }; + begin !== undefined && (node.loc = [begin, end]); + return node; } desugar(children, type) { // if type is given, the first element is expected to contain it as plain value @@ -247,27 +253,27 @@ export class MondoParser { return children; } parse_list() { - let children = this.parse_pair('open_list', 'close_list'); - children = this.desugar(children); - return { type: 'list', children }; + let node = this.parse_pair('open_list', 'close_list'); + node.children = this.desugar(node.children); + return node; } parse_angle() { - let children = this.parse_pair('open_angle', 'close_angle'); - children = [{ type: 'plain', value: 'angle' }, ...children]; - children = this.desugar(children, 'angle'); - return { type: 'list', children }; + let node = this.parse_pair('open_angle', 'close_angle'); + node.children.unshift({ type: 'plain', value: 'angle' }); + node.children = this.desugar(node.children, 'angle'); + return node; } parse_square() { - let children = this.parse_pair('open_square', 'close_square'); - children = [{ type: 'plain', value: 'square' }, ...children]; - children = this.desugar(children, 'square'); - return { type: 'list', children }; + let node = this.parse_pair('open_square', 'close_square'); + node.children.unshift({ type: 'plain', value: 'square' }); + node.children = this.desugar(node.children, 'square'); + return node; } parse_curly() { - let children = this.parse_pair('open_curly', 'close_curly'); - children = [{ type: 'plain', value: 'curly' }, ...children]; - children = this.desugar(children, 'curly'); - return { type: 'list', children }; + let node = this.parse_pair('open_curly', 'close_curly'); + node.children.unshift({ type: 'plain', value: 'curly' }); + node.children = this.desugar(node.children, 'curly'); + return node; } consume(type) { // shift removes first element and returns it @@ -317,10 +323,10 @@ export class MondoRunner { throw new Error(error); } } - run(code, offset = 0) { + run(code, scope, offset = 0) { const ast = this.parser.parse(code, offset); console.log(printAst(ast)); - return this.evaluate(ast); + return this.evaluate(ast, scope); } evaluate_def(ast, scope) { // (def name body) @@ -330,18 +336,19 @@ export class MondoRunner { const name = ast.children[1].value; const body = this.evaluate(ast.children[2], scope); scope[name] = body; - return this.evaluator(ast, scope); + // def with fall through } evaluate_lambda(ast, scope) { // (fn (_) (ply 2 _) // ^args ^ body - const [_, args, body] = ast.children; - const argNames = args.children.map((child) => child.value); - return (x) => { - scope = { - [argNames[0]]: x, // TODO: merge scope... + support multiple args + const [_, formalArgs, body] = ast.children; + return (...args) => { + const params = Object.fromEntries(formalArgs.children.map((arg, i) => [arg.value, args[i]])); + const closure = { + ...scope, + ...params, }; - return this.evaluate(body, scope); + return this.evaluate(body, closure); }; } evaluate_list(ast, scope) { @@ -362,12 +369,12 @@ export class MondoRunner { if (ast.type !== 'list') { return this.evaluate_leaf(ast, scope); } - if (ast.children[0]?.value === 'def') { - return this.evaluate_def(ast, scope); - } if (ast.children[0]?.value === 'fn') { return this.evaluate_lambda(ast, scope); } + if (ast.children[0]?.value === 'def') { + this.evaluate_def(ast, scope); + } return this.evaluate_list(ast, scope); } } diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index effaf367c..e995c0e4e 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -93,7 +93,7 @@ export function mondo(code, offset = 0) { if (Array.isArray(code)) { code = code.join(''); } - const pat = runner.run(code, offset); + const pat = runner.run(code, undefined, offset); return pat.markcss('color: var(--caret,--foreground);text-decoration:underline'); } From 077aac1888da1d9c4d307161158dfe50f875da67 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Tue, 1 Apr 2025 21:50:03 +0200 Subject: [PATCH 071/174] mondo: add some examples from sicp book --- packages/mondo/test/mondo.test.mjs | 84 ++++++++++++++++++++++++++---- 1 file changed, 73 insertions(+), 11 deletions(-) diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index 6f8090cc5..c9ec8aa02 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -121,6 +121,7 @@ describe('mondo sugar', () => { it('should desugar (.)', () => expect(desguar('(.)')).toEqual('(fn (_) _)')); it('should desugar lambda', () => expect(desguar('(.fast 2)')).toEqual('(fn (_) (fast 2 _))')); + it('should desugar lambda call', () => expect(desguar('((.mul 2) 2)')).toEqual('((fn (_) (mul 2 _)) 2)')); it('should desugar lambda with pipe', () => expect(desguar('(.fast 2 .room 1)')).toEqual('(fn (_) (room 1 (fast 2 _)))')); /* const lambda = parser.parse('(lambda (_) (fast 2 _))'); @@ -130,26 +131,87 @@ describe('mondo sugar', () => { }); describe('mondo arithmetic', () => { + let multi = + (op) => + (init, ...rest) => + rest.reduce((acc, arg) => op(acc, arg), init); + let lib = { - add: (a, b) => a + b, - mul: (a, b) => a * b, + '+': multi((a, b) => a + b), + '-': multi((a, b) => a - b), + '*': multi((a, b) => a * b), + '/': multi((a, b) => a / b), + run: (...args) => args[args.length - 1], + def: () => 0, PI: Math.PI, }; - function evaluator(node) { - // check if node is a leaf node (!= list) + function evaluator(node, scope) { if (node.type !== 'list') { - // check lib if we find a match in the lib, otherwise return value - return lib[node.value] ?? node.value; + // is leaf + return scope[node.value] ?? lib[node.value] ?? node.value; } - // now it can only be a list.. + // is list const [fn, ...args] = node.children; - // children in a list will already be evaluated - // the first child is expected to be a function if (typeof fn !== 'function') { - throw new Error(`"${fn}" is not a function ${typeof fn}`); + throw new Error(`"${fn}": expected function, got ${typeof fn} "${JSON.stringify(fn)}"`); } return fn(...args); } const runner = new MondoRunner({ evaluator }); - it('should desugar (.)', () => expect(runner.run('add 1 (mul 2 PI)').toFixed(2)).toEqual('7.28')); + let evaluate = (exp, scope) => runner.run(`run ${exp}`, scope); + let pretty = (exp) => printAst(runner.parser.parse(exp), false); + //it('should eval nested expression', () => expect(runner.run('add 1 (mul 2 PI)').toFixed(2)).toEqual('7.28')); + + it('eval number', () => expect(evaluate('2')).toEqual(2)); + it('eval string', () => expect(evaluate('abc')).toEqual('abc')); + it('eval list', () => expect(evaluate('(+ 1 2)')).toEqual(3)); + it('eval nested list', () => expect(evaluate('(+ 1 (+ 2 3))')).toEqual(6)); + it('def number', () => expect(evaluate('(def a 2) a')).toEqual(2)); + it('def + ref number', () => expect(evaluate('(def a 2) (* a a)')).toEqual(4)); + it('def + call lambda', () => expect(evaluate('(def sqr (fn (x) (* x x))) (sqr 3)')).toEqual(9)); + + // sicp + it('sicp 8.1', () => expect(evaluate('(+ 137 349)')).toEqual(486)); + it('sicp 8.2', () => expect(evaluate('(- 1000 334)')).toEqual(666)); + it('sicp 8.3', () => expect(evaluate('(* 5 99)')).toEqual(495)); + it('sicp 8.4', () => expect(evaluate('(/ 10 5)')).toEqual(2)); + it('sicp 8.5', () => expect(evaluate('(+ 2.7 10)')).toEqual(12.7)); + it('sicp 9.1', () => expect(evaluate('(+ 21 35 12 7)')).toEqual(75)); + it('sicp 9.2', () => expect(evaluate('(* 25 4 12)')).toEqual(1200)); + it('sicp 9.3', () => expect(evaluate('(+ (* 3 5) (- 10 6))')).toEqual(19)); + it('sicp 9.4', () => + expect(pretty('(+ (* 3 (+ (* 2 4) (+ 3 5))) (+ (- 10 7) 6))')).toEqual(`(+ + (* 3 + (+ + (* 2 4) + (+ 3 5) + ) + ) + (+ + (- 10 7) 6 + ) +)`)); // this is not exactly pretty printing by convention.. + + let scope = {}; + it('sicp 11.1', () => expect(evaluate('(def size 2) (* 5 size)', scope)).toEqual(10)); + it('sicp 11.2', () => + expect(evaluate('(def pi 3.14159) (def radius 10) (* pi (* radius radius))', scope)).toEqual(314.159)); + it('sicp 11.3', () => expect(evaluate('(def circumference (* 2 pi radius))', scope)).toEqual(0)); + it('sicp 11.4', () => expect(evaluate('circumference', scope)).toEqual(62.8318)); + it('sicp 13.1', () => expect(evaluate('(* (+ 2 (* 4 6)) (+ 3 5 7))')).toEqual(390)); + it('sicp 16.1', () => expect(evaluate('(def square (fn (x) (* x x)))', scope)).toEqual(0)); + // it('sicp 16.1', () => expect(evaluate('(def (square x) (* x x))', scope)).toEqual(0)); + it('sicp 17.1', () => expect(evaluate('(square 21)', scope)).toEqual(441)); + it('sicp 17.2', () => expect(evaluate('(square (+ 2 5))', scope)).toEqual(49)); + it('sicp 17.3', () => expect(evaluate('(square (square 3))', scope)).toEqual(81)); + it('sicp 17.4', () => + expect(evaluate(`(def sum-of-squares (fn (x y) (+ (square x) (square y))))`, scope)).toEqual(0)); + it('sicp 17.5', () => expect(evaluate(`(sum-of-squares 3 4)`, scope)).toEqual(25)); + it('sicp 17.6', () => + expect(evaluate(`(def f (fn (a) (sum-of-squares (+ a 1) (* a 2)))) (f 5)`, scope)).toEqual(136)); + it('sicp 21.1', () => expect(evaluate(`(sum-of-squares (+ 5 1) (* 5 2))`, scope)).toEqual(136)); + + /* it('sicp 11.1', () => expect(evaluate('(* 5 size)', { size: 3 })).toEqual(15)); + it('sicp 11.1', () => expect(evaluate('(def b 3) (* a b)', scope)).toEqual(12)); + it('sicp 11.1', () => expect(scope.b).toEqual(3)); */ }); From f6ffdd6ae6779698b1a84ab902d2771e9752ad15 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Tue, 1 Apr 2025 22:31:42 +0200 Subject: [PATCH 072/174] mondo: move +- to ops + add "raw" to parsed pairs + implement match + if + add more sicp examples --- packages/mondo/mondo.mjs | 57 +++++++++++++++++++++++++++--- packages/mondo/test/mondo.test.mjs | 43 ++++++++++++++++++---- 2 files changed, 89 insertions(+), 11 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 2553f74fb..cdbaf964a 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -19,12 +19,12 @@ export class MondoParser { open_curly: /^\{/, close_curly: /^\}/, number: /^-?[0-9]*\.?[0-9]+/, // before pipe! - op: /^[*/:!@%?]|^\.{2}/, // * / : ! @ % ? .. + op: /^[*/:!@%?+-]|^\.{2}/, // * / : ! @ % ? .. // dollar: /^\$/, pipe: /^\./, stack: /^[,$]/, or: /^[|]/, - plain: /^[a-zA-Z0-9-~_^#+-]+/, + plain: /^[a-zA-Z0-9-~_^#]+/, }; // matches next token next_token(code, offset = 0) { @@ -230,7 +230,10 @@ export class MondoParser { const end = this.tokens[0].loc?.[1]; this.consume(close_type); const node = { type: 'list', children }; - begin !== undefined && (node.loc = [begin, end]); + if (begin !== undefined) { + node.loc = [begin, end]; + node.raw = this.code.slice(begin, end); + } return node; } desugar(children, type) { @@ -338,6 +341,43 @@ export class MondoRunner { scope[name] = body; // def with fall through } + evaluate_match(ast, scope) { + // (match (p1 e1) (p2 e2) ... (pn en)) + // = cond in lisp + if (ast.children.length < 2) { + return; + } + const [_, ...body] = ast.children; + for (let i = 0; i < body.length; ++i) { + const [predicate, exp] = body[i].children; + if (predicate.value === 'else') { + return this.evaluate(exp, scope); + } + const outcome = this.evaluate(predicate, scope); + if (outcome) { + return this.evaluate(exp, scope); + } + } + return undefined; // nothing was matched + } + evaluate_if(ast, scope) { + // if is a special case of match + if (ast.children.length !== 4) { + return; + } + // (if predicate consequent alternative) + const [_, predicate, consequent, alternative] = ast.children; + // (match (predicate consequent) (else alternative)) + const matcher = { + type: 'list', + children: [ + { type: 'plain', value: 'match' }, + { type: 'list', children: [predicate, consequent] }, + { type: 'list', children: [{ type: 'plain', value: 'else' }, alternative] }, + ], + }; + return this.evaluate_match(matcher, scope); + } evaluate_lambda(ast, scope) { // (fn (_) (ply 2 _) // ^args ^ body @@ -369,10 +409,17 @@ export class MondoRunner { if (ast.type !== 'list') { return this.evaluate_leaf(ast, scope); } - if (ast.children[0]?.value === 'fn') { + const name = ast.children[0]?.value; + if (name === 'fn') { return this.evaluate_lambda(ast, scope); } - if (ast.children[0]?.value === 'def') { + if (name === 'match') { + return this.evaluate_match(ast, scope); + } + if (name === 'if') { + return this.evaluate_if(ast, scope); + } + if (name === 'def') { this.evaluate_def(ast, scope); } return this.evaluate_list(ast, scope); diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index c9ec8aa02..15cb42218 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -141,6 +141,12 @@ describe('mondo arithmetic', () => { '-': multi((a, b) => a - b), '*': multi((a, b) => a * b), '/': multi((a, b) => a / b), + eq: (a, b) => a === b, + lt: (a, b) => a < b, + gt: (a, b) => a > b, + and: (a, b) => a && b, + or: (a, b) => a || b, + not: (a) => !a, run: (...args) => args[args.length - 1], def: () => 0, PI: Math.PI, @@ -204,12 +210,37 @@ describe('mondo arithmetic', () => { it('sicp 17.1', () => expect(evaluate('(square 21)', scope)).toEqual(441)); it('sicp 17.2', () => expect(evaluate('(square (+ 2 5))', scope)).toEqual(49)); it('sicp 17.3', () => expect(evaluate('(square (square 3))', scope)).toEqual(81)); - it('sicp 17.4', () => - expect(evaluate(`(def sum-of-squares (fn (x y) (+ (square x) (square y))))`, scope)).toEqual(0)); - it('sicp 17.5', () => expect(evaluate(`(sum-of-squares 3 4)`, scope)).toEqual(25)); - it('sicp 17.6', () => - expect(evaluate(`(def f (fn (a) (sum-of-squares (+ a 1) (* a 2)))) (f 5)`, scope)).toEqual(136)); - it('sicp 21.1', () => expect(evaluate(`(sum-of-squares (+ 5 1) (* 5 2))`, scope)).toEqual(136)); + it('sicp 17.4', () => expect(evaluate(`(def sumofsquares (fn (x y) (+ (square x) (square y))))`, scope)).toEqual(0)); + it('sicp 17.5', () => expect(evaluate(`(sumofsquares 3 4)`, scope)).toEqual(25)); + it('sicp 17.6', () => expect(evaluate(`(def f (fn (a) (sumofsquares (+ a 1) (* a 2)))) (f 5)`, scope)).toEqual(136)); + it('sicp 21.1', () => expect(evaluate(`(sumofsquares (+ 5 1) (* 5 2))`, scope)).toEqual(136)); + + it('sicp 22.1', () => + expect( + evaluate( + `(def abs (fn (x) + (match + ((gt x 0) x) + ((eq x 0) 0) + ((lt x 0) (- 0 x)) + )))`, // sicp was doing (- x), which doesnt work with our - + scope, + ), + ).toEqual(0)); + + it('sicp gt1', () => expect(evaluate(`(gt -12 0)`, scope)).toEqual(false)); + it('sicp gt2', () => expect(evaluate(`(gt 0 -12)`, scope)).toEqual(true)); + it('sicp lt1', () => expect(evaluate(`(lt -12 0)`, scope)).toEqual(true)); + it('sicp lt2', () => expect(evaluate(`(lt 0 -12)`, scope)).toEqual(false)); + + it('sicp 24.1', () => expect(evaluate(`(abs (- 3))`, scope)).toEqual(3)); + it('sicp 24.2', () => expect(evaluate(`(abs (+ 3))`, scope)).toEqual(3)); + it('sicp 24.3', () => expect(evaluate(`(abs -12)`, scope)).toEqual(12)); + + it('sicp 24.4', () => expect(evaluate(`(def abs (fn (x) (if (lt x 0) (- 0 x) x)))`, scope)).toEqual(0)); + it('sicp 24.5', () => expect(evaluate(`(abs -13)`, scope)).toEqual(13)); + it('sicp 25.1', () => expect(evaluate(`(and (gt 6 5) (lt 6 10))`, scope)).toEqual(true)); + it('sicp 25.2', () => expect(evaluate(`(and (gt 4 5) (lt 6 10))`, scope)).toEqual(false)); /* it('sicp 11.1', () => expect(evaluate('(* 5 size)', { size: 3 })).toEqual(15)); it('sicp 11.1', () => expect(evaluate('(def b 3) (* a b)', scope)).toEqual(12)); From aa70246afcc484d70899886965df9b1d473dd6e1 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Tue, 1 Apr 2025 23:24:47 +0200 Subject: [PATCH 073/174] mondo: more sicp --- packages/mondo/test/mondo.test.mjs | 114 ++++++++++++++++++++++++++++- 1 file changed, 111 insertions(+), 3 deletions(-) diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index 15cb42218..d52fc004b 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -138,7 +138,9 @@ describe('mondo arithmetic', () => { let lib = { '+': multi((a, b) => a + b), + add: multi((a, b) => a + b), '-': multi((a, b) => a - b), + sub: multi((a, b) => a - b), '*': multi((a, b) => a * b), '/': multi((a, b) => a / b), eq: (a, b) => a === b, @@ -242,7 +244,113 @@ describe('mondo arithmetic', () => { it('sicp 25.1', () => expect(evaluate(`(and (gt 6 5) (lt 6 10))`, scope)).toEqual(true)); it('sicp 25.2', () => expect(evaluate(`(and (gt 4 5) (lt 6 10))`, scope)).toEqual(false)); - /* it('sicp 11.1', () => expect(evaluate('(* 5 size)', { size: 3 })).toEqual(15)); - it('sicp 11.1', () => expect(evaluate('(def b 3) (* a b)', scope)).toEqual(12)); - it('sicp 11.1', () => expect(scope.b).toEqual(3)); */ + it('sicp ex1.1.1', () => expect(evaluate(`(def a 3)`, scope)).toEqual(0)); + it('sicp ex1.1.2', () => expect(evaluate(`(def b (+ a 1))`, scope)).toEqual(0)); + it('sicp ex1.1.3', () => expect(evaluate(`(+ a b (* a b))`, scope)).toEqual(19)); + it('sicp ex1.1.4', () => expect(evaluate(`(if (and (gt b a) (lt b (* a b))) b a)`, scope)).toEqual(4)); + it('sicp ex1.1.5', () => expect(evaluate(`(match ((eq a 4) 6) ((eq b 4) (+ 6 7 a)) (else 25))`, scope)).toEqual(16)); + it('sicp ex1.1.6', () => expect(evaluate(`(+ 2 (if (gt b a) b a))`, scope)).toEqual(6)); + it('sicp ex1.1.7', () => + expect(evaluate(`(* (match ((gt a b) a) ((lt a b) b) (else -1)) (+ a 1))`, scope)).toEqual(16)); + + // .. cant use "+" and "-" as standalone expressions, because they are parsed as operators... + it('sicp ex1.4.1', () => expect(evaluate(`(def foo (fn (a b) ((if (gt b 0) add sub) a b)))`, scope)).toEqual(0)); + it('sicp ex1.4.1', () => expect(evaluate(`(foo 3 1)`, scope)).toEqual(4)); + it('sicp ex1.4.2', () => expect(evaluate(`(foo 3 -1)`, scope)).toEqual(4)); + + // 1.1.7 Example: Square Roots by Newton’s Method + it('sicp 30.1', () => + expect(evaluate(`(def goodenuf (fn (guess x) (lt (abs (- (square guess) x)) 0.001)))`, scope)).toEqual(0)); + it('sicp 30.2', () => expect(evaluate(`(goodenuf 1 1.001)`, scope)).toEqual(true)); + it('sicp 30.3', () => expect(evaluate(`(goodenuf 1 1.002)`, scope)).toEqual(false)); + it('sicp 30.4', () => expect(evaluate(`(def average (fn (x y) (/ (+ x y) 2)))`, scope)).toEqual(0)); + it('sicp 30.5', () => expect(evaluate(`(average 18 20)`, scope)).toEqual(19)); + it('sicp 30.6', () => expect(evaluate(`(def improve (fn (guess x) (average guess (/ x guess))))`, scope)).toEqual(0)); + it('sicp 31.1', () => + expect( + evaluate( + `(def sqrtiter (fn (guess x) (if (goodenuf guess x) + guess + (sqrtiter (improve guess x) x))))`, + scope, + ), + ).toEqual(0)); + it('sicp 31.2', () => expect(evaluate(`(def sqrt (fn (x) (sqrtiter 1.0 x)))`, scope)).toEqual(0)); + it('sicp 31.3', () => expect(evaluate(`(sqrt 9)`, scope)).toEqual(3.00009155413138)); + it('sicp 31.4', () => expect(evaluate(`(sqrt (+ 100 37))`, scope)).toEqual(11.704699917758145)); + it('sicp 31.5', () => expect(evaluate(`(sqrt (+ (sqrt 2) (sqrt 3)))`, scope)).toEqual(1.77392790232078925)); + it('sicp 31.6', () => expect(evaluate(`(square (sqrt 1000))`, scope)).toEqual(1000.000369924366)); + + // lexical scoping + // doesnt work... + /* it('sicp 39.1', () => + expect( + evaluate( + `(def sqrt (fn (x) +(def (goodenuf guess) +(lt (abs (- (square guess) x)) 0.001)) (def (improve guess) +(average guess (/ x guess))) (def sqrtiter (fn (guess) +(if (goodenuf guess) guess + (sqrtiter (improve guess))))) + (sqrtiter 1.0))) (sqrt 7)`, + scope, + ), + ).toEqual(0)); */ + + // recursive fac + it('sicp 41.1', () => expect(evaluate(`(def fac (fn (n) (if (eq n 1) 1 (* n (fac (- n 1))))))`, scope)).toEqual(0)); + it('sicp 41.2', () => expect(evaluate(`(fac 4)`, scope)).toEqual(24)); + + // iterative fac + // uses lexical scoping -> doesnt work + /* it('sicp 41.3', () => + expect( + evaluate( + `(def fac (fn (n) (faciter 1 1 n))) +(def faciter (fn (product counter maxcount) (if (gt counter maxcount) + product + (faciter (* counter product) + (+ counter 1) + max-count))))`, + scope, + ), + ).toEqual(0)); + it('sicp 41.4', () => expect(evaluate(`(fac 4)`, scope)).toEqual(24)); */ + + // 46.1 + /* (define (+ a b) +(if (= a 0) b (inc (+ (dec a) b)))) +(define (+ a b) +(if (= a 0) b (+ (dec a) (inc b)))) */ + + // Exercise 1.10 + // Ackermann’s function + it('sicp 47.1', () => + expect( + evaluate( + ` +(def A (fn (x y) (match ((eq y 0) 0) +((eq x 0) (* 2 y)) +((eq y 1) 2) +(else (A (- x 1) (A x (- y 1))))))) +`, + scope, + ), + ).toEqual(0)); + it('sicp 47.2', () => expect(evaluate(`(A 1 10)`, scope)).toEqual(1024)); + it('sicp 47.3', () => expect(evaluate(`(A 2 4)`, scope)).toEqual(65536)); + it('sicp 47.4', () => expect(evaluate(`(A 3 3)`, scope)).toEqual(65536)); + it('sicp 47.5', () => + expect( + evaluate( + ` +(def f (fn (n) (A 0 n))) +(def g (fn (n) (A 1 n))) +(def h (fn (n) (A 2 n))) +(def k (fn (n) (* 5 n n))) + `, + scope, + ), + ).toEqual(0)); + // Tree Recursion }); From 39d27844411f0ec88c25d733d64845bb2a750811 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 2 Apr 2025 22:09:17 +0200 Subject: [PATCH 074/174] mondo: more sicp tests + support special form for function def (might remove later) + support multiple expressions in fn body + support lexical scoping --- packages/mondo/mondo.mjs | 28 ++- packages/mondo/test/mondo.test.mjs | 302 ++++++++++++++++++++++++----- 2 files changed, 279 insertions(+), 51 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index cdbaf964a..8db36bd5d 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -328,10 +328,29 @@ export class MondoRunner { } run(code, scope, offset = 0) { const ast = this.parser.parse(code, offset); - console.log(printAst(ast)); + //console.log(printAst(ast)); return this.evaluate(ast, scope); } evaluate_def(ast, scope) { + // function definition special form? + if (ast.children[1].type === 'list') { + // (def (add a b) (+ a b)) + // => (def add (fn (a b) (+ a b)) ) + const args = ast.children[1].children.slice(1); + const lambda = { + // lambda + type: 'list', + children: [ + { type: 'plain', value: 'fn' }, + { type: 'list', children: args }, + ...ast.children.slice(2), // body + ], + }; + // we mutate to make sure the old ast wont make a mess later + ast.children[1] = ast.children[1].children[0]; + ast.children[2] = lambda; + ast.children = ast.children.slice(0, 3); // throw away rest + } // (def name body) if (ast.children.length !== 3) { throw new Error(`expected "def" to have 3 children, but got ${ast.children.length}`); @@ -381,14 +400,17 @@ export class MondoRunner { evaluate_lambda(ast, scope) { // (fn (_) (ply 2 _) // ^args ^ body - const [_, formalArgs, body] = ast.children; + const [_, formalArgs, ...body] = ast.children; return (...args) => { const params = Object.fromEntries(formalArgs.children.map((arg, i) => [arg.value, args[i]])); const closure = { ...scope, ...params, }; - return this.evaluate(body, closure); + // body can have multiple expressions + const res = body.map((exp) => this.evaluate(exp, closure)); + // last expression is the return value + return res[res.length - 1]; }; } evaluate_list(ast, scope) { diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index d52fc004b..d5b9a094f 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -143,6 +143,7 @@ describe('mondo arithmetic', () => { sub: multi((a, b) => a - b), '*': multi((a, b) => a * b), '/': multi((a, b) => a / b), + mod: multi((a, b) => a % b), eq: (a, b) => a === b, lt: (a, b) => a < b, gt: (a, b) => a > b, @@ -207,25 +208,25 @@ describe('mondo arithmetic', () => { it('sicp 11.3', () => expect(evaluate('(def circumference (* 2 pi radius))', scope)).toEqual(0)); it('sicp 11.4', () => expect(evaluate('circumference', scope)).toEqual(62.8318)); it('sicp 13.1', () => expect(evaluate('(* (+ 2 (* 4 6)) (+ 3 5 7))')).toEqual(390)); - it('sicp 16.1', () => expect(evaluate('(def square (fn (x) (* x x)))', scope)).toEqual(0)); + it('sicp 16.1', () => expect(evaluate('(def (square x) (* x x))', scope)).toEqual(0)); // it('sicp 16.1', () => expect(evaluate('(def (square x) (* x x))', scope)).toEqual(0)); it('sicp 17.1', () => expect(evaluate('(square 21)', scope)).toEqual(441)); it('sicp 17.2', () => expect(evaluate('(square (+ 2 5))', scope)).toEqual(49)); it('sicp 17.3', () => expect(evaluate('(square (square 3))', scope)).toEqual(81)); - it('sicp 17.4', () => expect(evaluate(`(def sumofsquares (fn (x y) (+ (square x) (square y))))`, scope)).toEqual(0)); + it('sicp 17.4', () => expect(evaluate(`(def (sumofsquares x y) (+ (square x) (square y)))`, scope)).toEqual(0)); it('sicp 17.5', () => expect(evaluate(`(sumofsquares 3 4)`, scope)).toEqual(25)); - it('sicp 17.6', () => expect(evaluate(`(def f (fn (a) (sumofsquares (+ a 1) (* a 2)))) (f 5)`, scope)).toEqual(136)); + it('sicp 17.6', () => expect(evaluate(`(def (f a) (sumofsquares (+ a 1) (* a 2))) (f 5)`, scope)).toEqual(136)); it('sicp 21.1', () => expect(evaluate(`(sumofsquares (+ 5 1) (* 5 2))`, scope)).toEqual(136)); it('sicp 22.1', () => expect( evaluate( - `(def abs (fn (x) + `(def (abs x) (match ((gt x 0) x) ((eq x 0) 0) ((lt x 0) (- 0 x)) - )))`, // sicp was doing (- x), which doesnt work with our - + ))`, // sicp was doing (- x), which doesnt work with our - scope, ), ).toEqual(0)); @@ -239,7 +240,7 @@ describe('mondo arithmetic', () => { it('sicp 24.2', () => expect(evaluate(`(abs (+ 3))`, scope)).toEqual(3)); it('sicp 24.3', () => expect(evaluate(`(abs -12)`, scope)).toEqual(12)); - it('sicp 24.4', () => expect(evaluate(`(def abs (fn (x) (if (lt x 0) (- 0 x) x)))`, scope)).toEqual(0)); + it('sicp 24.4', () => expect(evaluate(`(def (abs x) (if (lt x 0) (- 0 x) x))`, scope)).toEqual(0)); it('sicp 24.5', () => expect(evaluate(`(abs -13)`, scope)).toEqual(13)); it('sicp 25.1', () => expect(evaluate(`(and (gt 6 5) (lt 6 10))`, scope)).toEqual(true)); it('sicp 25.2', () => expect(evaluate(`(and (gt 4 5) (lt 6 10))`, scope)).toEqual(false)); @@ -254,68 +255,73 @@ describe('mondo arithmetic', () => { expect(evaluate(`(* (match ((gt a b) a) ((lt a b) b) (else -1)) (+ a 1))`, scope)).toEqual(16)); // .. cant use "+" and "-" as standalone expressions, because they are parsed as operators... - it('sicp ex1.4.1', () => expect(evaluate(`(def foo (fn (a b) ((if (gt b 0) add sub) a b)))`, scope)).toEqual(0)); + it('sicp ex1.4.1', () => expect(evaluate(`(def (foo a b) ((if (gt b 0) add sub) a b))`, scope)).toEqual(0)); it('sicp ex1.4.1', () => expect(evaluate(`(foo 3 1)`, scope)).toEqual(4)); it('sicp ex1.4.2', () => expect(evaluate(`(foo 3 -1)`, scope)).toEqual(4)); // 1.1.7 Example: Square Roots by Newton’s Method it('sicp 30.1', () => - expect(evaluate(`(def goodenuf (fn (guess x) (lt (abs (- (square guess) x)) 0.001)))`, scope)).toEqual(0)); + expect(evaluate(`(def (goodenuf guess x) (lt (abs (- (square guess) x)) 0.001))`, scope)).toEqual(0)); it('sicp 30.2', () => expect(evaluate(`(goodenuf 1 1.001)`, scope)).toEqual(true)); it('sicp 30.3', () => expect(evaluate(`(goodenuf 1 1.002)`, scope)).toEqual(false)); - it('sicp 30.4', () => expect(evaluate(`(def average (fn (x y) (/ (+ x y) 2)))`, scope)).toEqual(0)); + it('sicp 30.4', () => expect(evaluate(`(def (average x y) (/ (+ x y) 2))`, scope)).toEqual(0)); it('sicp 30.5', () => expect(evaluate(`(average 18 20)`, scope)).toEqual(19)); - it('sicp 30.6', () => expect(evaluate(`(def improve (fn (guess x) (average guess (/ x guess))))`, scope)).toEqual(0)); + it('sicp 30.6', () => expect(evaluate(`(def (improve guess x) (average guess (/ x guess)))`, scope)).toEqual(0)); it('sicp 31.1', () => expect( evaluate( - `(def sqrtiter (fn (guess x) (if (goodenuf guess x) + `(def (sqrtiter guess x) (if (goodenuf guess x) guess - (sqrtiter (improve guess x) x))))`, + (sqrtiter (improve guess x) x)))`, scope, ), ).toEqual(0)); - it('sicp 31.2', () => expect(evaluate(`(def sqrt (fn (x) (sqrtiter 1.0 x)))`, scope)).toEqual(0)); + it('sicp 31.2', () => expect(evaluate(`(def (sqrt x) (sqrtiter 1.0 x))`, scope)).toEqual(0)); it('sicp 31.3', () => expect(evaluate(`(sqrt 9)`, scope)).toEqual(3.00009155413138)); it('sicp 31.4', () => expect(evaluate(`(sqrt (+ 100 37))`, scope)).toEqual(11.704699917758145)); it('sicp 31.5', () => expect(evaluate(`(sqrt (+ (sqrt 2) (sqrt 3)))`, scope)).toEqual(1.77392790232078925)); it('sicp 31.6', () => expect(evaluate(`(square (sqrt 1000))`, scope)).toEqual(1000.000369924366)); // lexical scoping - // doesnt work... - /* it('sicp 39.1', () => + it('sicp 39.1', () => expect( evaluate( - `(def sqrt (fn (x) -(def (goodenuf guess) -(lt (abs (- (square guess) x)) 0.001)) (def (improve guess) -(average guess (/ x guess))) (def sqrtiter (fn (guess) -(if (goodenuf guess) guess - (sqrtiter (improve guess))))) - (sqrtiter 1.0))) (sqrt 7)`, - scope, - ), - ).toEqual(0)); */ - - // recursive fac - it('sicp 41.1', () => expect(evaluate(`(def fac (fn (n) (if (eq n 1) 1 (* n (fac (- n 1))))))`, scope)).toEqual(0)); - it('sicp 41.2', () => expect(evaluate(`(fac 4)`, scope)).toEqual(24)); - - // iterative fac - // uses lexical scoping -> doesnt work - /* it('sicp 41.3', () => - expect( - evaluate( - `(def fac (fn (n) (faciter 1 1 n))) -(def faciter (fn (product counter maxcount) (if (gt counter maxcount) - product - (faciter (* counter product) - (+ counter 1) - max-count))))`, + ` +(def (sqrt x) + (def (goodenough guess) + (lt (abs (- (square guess) x)) 0.001)) +(def (improve guess) + (average guess (/ x guess))) +(def (sqrt-iter guess) + (if (goodenough guess) guess (sqrt-iter (improve guess)))) +(sqrtiter 1.0)) + + `, scope, ), ).toEqual(0)); - it('sicp 41.4', () => expect(evaluate(`(fac 4)`, scope)).toEqual(24)); */ + + // recursive fac + it('sicp 41.1', () => expect(evaluate(`(def (fac n) (if (eq n 1) 1 (* n (fac (- n 1)))))`, scope)).toEqual(0)); + it('sicp 41.2', () => expect(evaluate(`(fac 4)`, scope)).toEqual(24)); + + // iterative fac + it('sicp 41.3', () => + expect( + evaluate( + ` +(def (factorial n) (factiter 1 1 n)) +(def (factiter product counter maxcount) + (if (gt counter maxcount) + product + (factiter (* counter product) + (+ counter 1) + maxcount))) +`, + scope, + ), + ).toEqual(0)); + it('sicp 41.4', () => expect(evaluate(`(fac 4)`, scope)).toEqual(24)); // 46.1 /* (define (+ a b) @@ -329,10 +335,10 @@ describe('mondo arithmetic', () => { expect( evaluate( ` -(def A (fn (x y) (match ((eq y 0) 0) +(def (A x y) (match ((eq y 0) 0) ((eq x 0) (* 2 y)) ((eq y 1) 2) -(else (A (- x 1) (A x (- y 1))))))) +(else (A (- x 1) (A x (- y 1)))))) `, scope, ), @@ -344,13 +350,213 @@ describe('mondo arithmetic', () => { expect( evaluate( ` -(def f (fn (n) (A 0 n))) -(def g (fn (n) (A 1 n))) -(def h (fn (n) (A 2 n))) -(def k (fn (n) (* 5 n n))) +(def (f n) (A 0 n)) +(def (g n) (A 1 n))) +(def (h n) (A 2 n)) +(def (k n) (* 5 n n)) `, scope, ), ).toEqual(0)); + // Tree Recursion + // recursive process + it('sicp 48.1', () => + expect( + evaluate( + ` +(def (fib n) (match ((eq n 0) 0) ((eq n 1) 1) +(else (+ (fib (- n 1)) (fib (- n 2)))))) +(fib 7) + `, + scope, + ), + ).toEqual(13)); + + // iterative process + it('sicp 48.2', () => + expect( + evaluate( + ` +(def (fib n) (fibiter 1 0 n)) +(def (fibiter a b count) (if (eq count 0) + b + (fibiter (+ a b) a (- count 1)))) +(fib 7) + `, + scope, + ), + ).toEqual(13)); + + // example: counting change + it('sicp 52.2', () => + expect( + evaluate( + ` +(def (countchange amount) (cc amount 5)) +(def (cc amount kindsofcoins) + (match + ((eq amount 0) 1) + ((or (lt amount 0) (eq kindsofcoins 0)) 0) + (else (+ + (cc amount (- kindsofcoins 1)) + (cc (- amount (firstdenomination kindsofcoins)) kindsofcoins))))) + +(def (firstdenomination kindsofcoins) +(match + ((eq kindsofcoins 1) 1) + ((eq kindsofcoins 2) 5) + ((eq kindsofcoins 3) 10) + ((eq kindsofcoins 4) 25) + ((eq kindsofcoins 5) 50))) + +(countchange 100) + `, + scope, + ), + ).toEqual(292)); + + // todo: pascals triangle + it('sicp 57.1', () => + expect( + evaluate( + ` +(def (cube x) (* x x x)) +(def (p x) (sub (* 3 x) (* 4 (cube x)))) +(def (sine angle) +(if (not (gt (abs angle) 0.1)) angle +(p (sine (/ angle 3.0))))) + +(sine 12.15) + `, + scope, + ), + ).toEqual(-0.39980345741334)); + + // exponentiation recursive + it('sicp 57.2', () => + expect( + evaluate( + ` +(def (expt b n) (if (eq n 0) 1 (* b (expt b (- n 1))))) +(expt 2 4) +`, + scope, + ), + ).toEqual(16)); + + // exponentiation iterative + it('sicp 58.1b', () => + expect( + evaluate( + ` +(def (expt b n) (exptiter b n 1)) +(def (exptiter b counter product) (if (eq counter 0) + product + (exptiter b (- counter 1) (* b product)))) +(expt 2 5) + `, + scope, + ), + ).toEqual(32)); + + // exponentiation fast + it('sicp 58.2', () => + expect( + evaluate( + ` +(def (fastexpt b n) (match ((eq n 0) 1) +((iseven n) (square (fastexpt b (/ n 2)))) (else (* b (fastexpt b (- n 1)))))) +(def (iseven n) +(eq (mod n 2) 0)) +(fastexpt 2 5) + `, + scope, + ), + ).toEqual(32)); + + // * = repeated addition + it('sicp 60.1', () => + expect( + evaluate( + `(def (mult a b) (if (eq b 0) + 0 + (+ a (* a (- b 1))))) + (mult 3 15) + `, + ), + ).toEqual(45)); + + // gcd / euclid + it('sicp 63.1', () => + expect( + evaluate( + `(def (gcd a b) (if (eq b 0) + a + (gcd b (mod a b)))) + (gcd 20 6) + `, + ), + ).toEqual(2)); + + // 65 smallest divisor + // 67 fermat test + // .... + + // higher order procedures + + it('sicp 77.1', () => + expect( + evaluate( + ` +(def (sum term a next b) +(if (gt a b) 0 (+ (term a) +(sum term (next a) next b)))) +`, + scope, + ), + ).toEqual(0)); + + it('sicp 78.1', () => + expect( + evaluate( + ` +(def (inc n) (+ n 1)) +(def (cube a) (* a a a)) +(def (sumcubes a b) +(sum cube a inc b)) +(sumcubes 1 10) + `, + scope, + ), + ).toEqual(3025)); + + it('sicp 78.2', () => + expect( + evaluate( + ` + (def (identity x) x) + (def (sumintegers a b) + (sum identity a inc b)) + (sumintegers 1 10) + `, + scope, + ), + ).toEqual(55)); + + // pisum pulled out defs to make it work + it('sicp 79.1', () => + expect( + evaluate( + ` +(def (piterm x) +(/ 1.0 (* x (+ x 2)))) +(def (pinext x) (+ x 4)) +(def (pisum a b) +(sum piterm a pinext b)) +(* 8 (pisum 1 1000)) +`, + scope, + ), + ).toEqual(3.139592655589783)); }); From b2588e8c935a6e066c449a5749fabe8e9caf83b0 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 2 Apr 2025 22:11:06 +0200 Subject: [PATCH 075/174] fix: lint --- packages/mondo/test/mondo.test.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index d5b9a094f..9f7f16be4 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -279,6 +279,7 @@ describe('mondo arithmetic', () => { it('sicp 31.2', () => expect(evaluate(`(def (sqrt x) (sqrtiter 1.0 x))`, scope)).toEqual(0)); it('sicp 31.3', () => expect(evaluate(`(sqrt 9)`, scope)).toEqual(3.00009155413138)); it('sicp 31.4', () => expect(evaluate(`(sqrt (+ 100 37))`, scope)).toEqual(11.704699917758145)); + // eslint-disable-next-line no-loss-of-precision it('sicp 31.5', () => expect(evaluate(`(sqrt (+ (sqrt 2) (sqrt 3)))`, scope)).toEqual(1.77392790232078925)); it('sicp 31.6', () => expect(evaluate(`(square (sqrt 1000))`, scope)).toEqual(1000.000369924366)); From 60d09adb449409842734a12a5ef4a545574528d0 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 2 Apr 2025 22:33:18 +0200 Subject: [PATCH 076/174] mondo: more tests --- packages/mondo/test/mondo.test.mjs | 58 +++++++++++++++++++++++++++--- 1 file changed, 54 insertions(+), 4 deletions(-) diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index 9f7f16be4..0fac441e8 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -545,19 +545,69 @@ describe('mondo arithmetic', () => { ), ).toEqual(55)); - // pisum pulled out defs to make it work + // pisum it('sicp 79.1', () => expect( evaluate( ` -(def (piterm x) -(/ 1.0 (* x (+ x 2)))) -(def (pinext x) (+ x 4)) (def (pisum a b) + (def (piterm x) + (/ 1.0 (* x (+ x 2)))) +(def (pinext x) (+ x 4)) (sum piterm a pinext b)) (* 8 (pisum 1 1000)) `, scope, ), ).toEqual(3.139592655589783)); + + // integral + it('sicp 79.2', () => + expect( + evaluate( + ` +(def (integral f a b dx) + (def (adddx x) (+ x dx)) +(* (sum f (+ a (/ dx 2.0)) adddx b) dx)) +(integral cube 0 1 0.01) + `, + scope, + ), + ).toEqual(0.24998750000000042)); + // maximum callstack... + //it('sicp 79.3', () => expect(evaluate(`(integral cube 0 1 0.001)`, scope)).toEqual(0.249999875000001)); + + //lambdas + it('sicp 83.1', () => expect(evaluate(`((fn (x) (+ x 4)) 5)`)).toEqual(9)); + + it('sicp 83.2', () => + expect( + evaluate( + ` +(def (pisum a b) + (sum (fn (x) (/ 1.0 (* x (+ x 2)))) + a + (fn (x) (+ x 4)) + b)) +(* 8 (pisum 1 1000)) +`, + scope, + ), + ).toEqual(3.139592655589783)); + it('sicp 83.3', () => + expect( + evaluate( + ` +(def (integral f a b dx) + (* (sum f + (+ a (/ dx 2.0)) + (fn (x) (+ x dx)) + b) + dx)) +(integral cube 0 1 0.01) +`, + scope, + ), + ).toEqual(0.24998750000000042)); + it('sicp 84.1', () => expect(evaluate(`((fn (x y z) (+ x y (square z))) 1 2 3)`, scope)).toEqual(12)); }); From 989bd0990e638f19d694826c5509683459f1a6b9 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 3 Apr 2025 17:01:36 +0200 Subject: [PATCH 077/174] mondo: let expressions --- packages/mondo/mondo.mjs | 16 +++++++++++ packages/mondo/test/mondo.test.mjs | 43 ++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 8db36bd5d..705b9de91 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -331,6 +331,19 @@ export class MondoRunner { //console.log(printAst(ast)); return this.evaluate(ast, scope); } + evaluate_let(ast, scope) { + // (let ((x 3) (y 4)) ...body) + // = ((fn (x y) ...body) 3 4) + const defs = ast.children[1].children; + const args = defs.map((pair) => pair.children[0]); + const vals = defs.map((pair) => pair.children[1]); + const body = ast.children.slice(2); + const lambda = { + type: 'list', + children: [{ type: 'plain', value: 'fn' }, { type: 'list', children: args }, ...body], + }; + return this.evaluate({ type: 'list', children: [lambda, ...vals] }, scope); + } evaluate_def(ast, scope) { // function definition special form? if (ast.children[1].type === 'list') { @@ -441,6 +454,9 @@ export class MondoRunner { if (name === 'if') { return this.evaluate_if(ast, scope); } + if (name === 'let') { + return this.evaluate_let(ast, scope); + } if (name === 'def') { this.evaluate_def(ast, scope); } diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index 0fac441e8..236fa2911 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -610,4 +610,47 @@ describe('mondo arithmetic', () => { ), ).toEqual(0.24998750000000042)); it('sicp 84.1', () => expect(evaluate(`((fn (x y z) (+ x y (square z))) 1 2 3)`, scope)).toEqual(12)); + + // let expressions + it('sicp 87.1', () => + expect( + evaluate( + ` +(+ (let ((x 3)) +(+ x (* x 10))) x) +`, + { x: 5 }, + ), + ).toEqual(38)); + it('sicp 87.2', () => + expect( + evaluate( + ` +(let ((x 3) +(y (+ x 2))) +(* x y)) + `, + { x: 2 }, + ), + ).toEqual(12)); + it('sicp 88.1', () => + expect( + evaluate( + ` +(def (f g) (g 2)) +(f square) + `, + scope, + ), + ).toEqual(4)); + it('sicp 88.2', () => + expect( + evaluate( + ` +(def (f g) (g 2)) +(f (fn (z) (* z (+ z 1)))) + `, + scope, + ), + ).toEqual(6)); }); From d4733a30aba4d6ec9dfaf53046b1e3ac0193fbc7 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 3 Apr 2025 17:30:03 +0200 Subject: [PATCH 078/174] mondo: add half interval method example --- packages/mondo/test/mondo.test.mjs | 45 ++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index 236fa2911..9c2931966 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -152,6 +152,7 @@ describe('mondo arithmetic', () => { not: (a) => !a, run: (...args) => args[args.length - 1], def: () => 0, + sin: Math.sin, PI: Math.PI, }; function evaluator(node, scope) { @@ -647,10 +648,48 @@ describe('mondo arithmetic', () => { expect( evaluate( ` -(def (f g) (g 2)) -(f (fn (z) (* z (+ z 1)))) - `, + (def (f g) (g 2)) + (f (fn (z) (* z (+ z 1)))) + `, scope, ), ).toEqual(6)); + + // Finding roots of equations by the half-interval method + it('sicp 89.1', () => + expect( + evaluate( + ` +(def (search f negpoint pospoint) + (let ((midpoint (average negpoint pospoint))) + (if (closeenough negpoint pospoint) + midpoint + (let ((testvalue (f midpoint))) + (match ((positive testvalue) + (search f negpoint midpoint)) + ((negative testvalue) + (search f midpoint pospoint)) + (else midpoint)))))) + +(def (closeenough x y) (lt (abs (- x y)) 0.001)) + +(def (negative x) (lt x 0)) +(def (positive x) (gt x 0)) + +(def (halfintervalmethod f a b) (let ((avalue (f a)) +(bvalue (f b))) +(match ((and (negative avalue) (positive bvalue)) +(search f a b)) +((and (negative bvalue) (positive avalue)) +(search f b a)) (else +(error "Values are not of opposite sign" a b))))) + +(halfintervalmethod sin 2.0 4.0) +`, + scope, + ), + ).toEqual(3.14111328125)); + + it('sicp 89.1', () => + expect(evaluate(`(halfintervalmethod (fn (x) (- (* x x x) (* 2 x) 3)) 1.0 2.0)`, scope)).toEqual(1.89306640625)); }); From d5ef686fe0fc3036363502e31556007412a64fa4 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 4 Apr 2025 00:45:00 +0200 Subject: [PATCH 079/174] mondo: sicp 100 --- packages/mondo/test/mondo.test.mjs | 66 ++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index 9c2931966..c685bcb28 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -153,6 +153,7 @@ describe('mondo arithmetic', () => { run: (...args) => args[args.length - 1], def: () => 0, sin: Math.sin, + cos: Math.cos, PI: Math.PI, }; function evaluator(node, scope) { @@ -692,4 +693,69 @@ describe('mondo arithmetic', () => { it('sicp 89.1', () => expect(evaluate(`(halfintervalmethod (fn (x) (- (* x x x) (* 2 x) 3)) 1.0 2.0)`, scope)).toEqual(1.89306640625)); + + // Finding fixed points of functions + it('sicp 92.1', () => + expect( + evaluate( + ` +(def tolerance 0.00001) +(def (fixedpoint f first-guess) + (def (closeenough v1 v2) (lt (abs (- v1 v2)) tolerance)) + (def (try guess) + (let ((next (f guess))) + (if (closeenough guess next) next (try next)))) + (try first-guess)) + +(fixedpoint cos 1.0) +`, + scope, + ), + ).toEqual(0.7390822985224023)); + it('sicp 93.1', () => + expect(evaluate(`(fixedpoint (fn (y) (+ (sin y) (cos y))) 1.0)`, scope)).toEqual(1.2587315962971173)); + // Maximum call stack size exceeded (expected) + /* it('sicp 93.2', () => + expect(evaluate(`(def (sqrt x) (fixedpoint (fn (y) (/ x y)) 1.0)) (sqrt 4)`, scope)).toEqual(0)); */ + it('sicp 93.3', () => + expect(evaluate(`(def (sqrt x) (fixedpoint (fn (y) (average y (/ x y))) 1.0)) (sqrt 7)`, scope)).toEqual( + 2.6457513110645907, + )); + // Procedures as Returned Values + it('sicp 97.1', () => + expect(evaluate(`(def (averagedamp f) (fn (x) (average x (f x)))) ((averagedamp square) 10)`, scope)).toEqual(55)); + it('sicp 98.1', () => + expect(evaluate(`(def (sqrt x) (fixedpoint (averagedamp (fn (y) (/ x y))) 1.0)) (sqrt 7)`, scope)).toEqual( + 2.6457513110645907, + )); + it('sicp 98.2', () => + expect( + evaluate(`(def (cuberoot x) (fixedpoint (averagedamp (fn (y) (/ x (square y)))) 1.0)) (cuberoot 7)`, scope), + ).toEqual(1.912934258514886)); + it('sicp 99.1', () => + expect( + evaluate( + ` + (def (deriv g) (fn (x) (/ (- (g (+ x dx)) (g x)) dx))) + (def dx 0.00001) + (def (cube x) (* x x x)) + ((deriv cube) 5) + `, + scope, + ), + ).toEqual(75.00014999664018)); + // With the aid of deriv, we can express Newton’s method as a fixed-point process: + it('sicp 100.1', () => + expect( + evaluate( + ` +(def (newtontransform g) +(fn (x) (- x (/ (g x) ((deriv g) x))))) +(def (newtonsmethod g guess) (fixedpoint (newtontransform g) guess)) +(def (sqrt x) (newtonsmethod (fn (y) (- (square y) x)) 1.0)) +(sqrt 7) + `, + scope, + ), + ).toEqual(2.6457513110645907)); }); From 7fc21d55d7904f5047806ef50e1ddbd308fde8c5 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 4 Apr 2025 10:04:22 +0200 Subject: [PATCH 080/174] mondo: rational number test --- packages/mondo/mondo.mjs | 1 + packages/mondo/test/mondo.test.mjs | 113 ++++++++++++++++++++++++++++- 2 files changed, 111 insertions(+), 3 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 705b9de91..bed568661 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -437,6 +437,7 @@ export class MondoRunner { ast.value = Number(ast.value); } else if (['quotes_double', 'quotes_single'].includes(ast.type)) { ast.value = ast.value.slice(1, -1); + ast.type = 'plain'; // is this problematic? } return this.evaluator(ast, scope); } diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index c685bcb28..7029c5772 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -155,6 +155,13 @@ describe('mondo arithmetic', () => { sin: Math.sin, cos: Math.cos, PI: Math.PI, + cons: (a, b) => [a, b], + car: (pair) => pair[0], + cdr: (pair) => pair[1], + concat: (...msgs) => msgs.join(''), + error: (...msgs) => { + throw new Error(msgs.join(' ')); + }, }; function evaluator(node, scope) { if (node.type !== 'list') { @@ -164,7 +171,7 @@ describe('mondo arithmetic', () => { // is list const [fn, ...args] = node.children; if (typeof fn !== 'function') { - throw new Error(`"${fn}": expected function, got ${typeof fn} "${JSON.stringify(fn)}"`); + throw new Error(`"${fn}": expected function, got ${typeof fn} "${fn}"`); } return fn(...args); } @@ -327,9 +334,9 @@ describe('mondo arithmetic', () => { it('sicp 41.4', () => expect(evaluate(`(fac 4)`, scope)).toEqual(24)); // 46.1 - /* (define (+ a b) + /* (def (+ a b) (if (= a 0) b (inc (+ (dec a) b)))) -(define (+ a b) +(def (+ a b) (if (= a 0) b (+ (dec a) (inc b)))) */ // Exercise 1.10 @@ -499,6 +506,7 @@ describe('mondo arithmetic', () => { (gcd b (mod a b)))) (gcd 20 6) `, + scope, ), ).toEqual(2)); @@ -758,4 +766,103 @@ describe('mondo arithmetic', () => { scope, ), ).toEqual(2.6457513110645907)); + // whatever this is + it('sicp 101.1', () => + expect( + evaluate( + ` + (def (fixedpointoftransform g transform guess) (fixedpoint (transform g) guess)) + (def (sqrt x) (fixedpointoftransform + (fn (y) (/ x y)) averagedamp 1.0)) + (sqrt 7) + `, + scope, + ), + ).toEqual(2.6457513110645907)); + it('sicp 101.2', () => + expect( + evaluate( + ` +(def (sqrt x) (fixedpointoftransform +(fn (y) (- (square y) x)) newtontransform 1.0)) +(sqrt 7) + `, + scope, + ), + ).toEqual(2.6457513110645907)); + + // data abstraction + + // rational arithmetic + it('sicp 114.1', () => + expect( + evaluate( + ` +(def (addrat x y) + (makerat (+ (* + (numer x) (denom y)) + (* (numer y) (denom x))) + (* (denom x) (denom y)))) +(def (subrat x y) + (makerat (- (* (numer x) (denom y)) + (* (numer y) (denom x))) + (* (denom x) (denom y)))) +(def (mulrat x y) + (makerat (* (numer x) (numer y)) + (* (denom x) (denom y)))) +(def (divrat x y) + (makerat (* (numer x) (denom y)) + (* (denom x) (numer y)))) +(def (equalrat x y) + (eq (* (numer x) (denom y)) + (* (numer y) (denom x)))) + `, + scope, + ), + ).toEqual(0)); + + // markerat number denom + it('sicp 117.1', () => + expect( + evaluate( + ` +(def (makerat n d) (cons n d)) +(def (numer x) (car x)) +(def (denom x) (cdr x)) +(def (printrat x) (concat (numer x) ':' (denom x))) + `, + scope, + ), + ).toEqual(0)); + + it('sicp 117.1', () => expect(evaluate(`(def onehalf (makerat 1 2)) (printrat onehalf)`, scope)).toEqual('1:2')); + it('sicp 117.2', () => + expect(evaluate(`(def onethird (makerat 1 3)) (printrat (addrat onehalf onethird))`, scope)).toEqual('5:6')); + it('sicp 117.3', () => expect(evaluate(`(printrat (mulrat onehalf onethird))`, scope)).toEqual('1:6')); + it('sicp 117.4', () => expect(evaluate(`(printrat (addrat onethird onethird))`, scope)).toEqual('6:9')); + it('sicp 118.1', () => + expect(evaluate(`(def (makerat n d) (let ((g (gcd n d))) (cons (/ n g) (/ d g))))`, scope)).toEqual(0)); + it('sicp 118.1', () => expect(evaluate(`(printrat (addrat onethird onethird))`, scope)).toEqual('2:3')); + + let lscope = {}; + // pairs with lambda + it('sicp 124.1', () => + expect( + evaluate( + ` +(def (cons x y) + (def (dispatch m) + (match + ((eq m 0) x) + ((eq m 1) y) + (else (error "argument not 0 or 1: CONS" m))) + ) dispatch) + (def (car z) (z 0)) + (def (cdr z) (z 1)) + `, + lscope, + ), + ).toEqual(0)); + it('sicp 124.1', () => expect(evaluate(`(car (cons first second))`, lscope)).toEqual('first')); + it('sicp 124.2', () => expect(evaluate(`(cdr (cons first second))`, lscope)).toEqual('second')); }); From 243adda3ddc2627eb51c3f93f8b2280d4da7ef12 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 1 May 2025 10:56:33 +0200 Subject: [PATCH 081/174] mondo: more sicp tests --- packages/mondo/test/mondo.test.mjs | 106 ++++++++++++++++++++++++++++- 1 file changed, 104 insertions(+), 2 deletions(-) diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index 7029c5772..4ec18decb 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -155,9 +155,12 @@ describe('mondo arithmetic', () => { sin: Math.sin, cos: Math.cos, PI: Math.PI, - cons: (a, b) => [a, b], + cons: (a, b) => [a, ...(Array.isArray(b) ? b : [b])], car: (pair) => pair[0], - cdr: (pair) => pair[1], + cdr: (pair) => pair.slice(1), + list: (...items) => items, + nil: [], + isnull: (items) => items.length === 0, concat: (...msgs) => msgs.join(''), error: (...msgs) => { throw new Error(msgs.join(' ')); @@ -865,4 +868,103 @@ describe('mondo arithmetic', () => { ).toEqual(0)); it('sicp 124.1', () => expect(evaluate(`(car (cons first second))`, lscope)).toEqual('first')); it('sicp 124.2', () => expect(evaluate(`(cdr (cons first second))`, lscope)).toEqual('second')); + // lists + it('sicp 135.1', () => expect(evaluate(`(list 1 2 3 4)`)).toEqual([1, 2, 3, 4])); + it('sicp 137.1', () => expect(evaluate(`(car (list 1 2 3 4))`)).toEqual(1)); + it('sicp 137.2', () => expect(evaluate(`(cdr (list 1 2 3 4))`)).toEqual([2, 3, 4])); + it('sicp 137.3', () => expect(evaluate(`(car (cdr (list 1 2 3 4)))`)).toEqual(2)); + it('sicp 137.4', () => expect(evaluate(`(cons 10 (list 1 2 3 4))`)).toEqual([10, 1, 2, 3, 4])); + // listref + it('sicp 138.1', () => + expect( + evaluate( + ` +(def (listref items n) (if (eq n 0) (car items) + (listref (cdr items) (- n 1)))) +(def squares (list 1 4 9 16 25)) +(listref squares 3)`, + scope, + ), + ).toEqual(16)); + // length recursive + it('sicp 138.2', () => + expect( + evaluate( + ` + (def (length items) + (if (isnull items) 0 + (+ 1 (length (cdr items))))) + (def odds (list 1 3 5 7)) + (length odds)`, + ), + ).toEqual(4)); + // length iterative + it('sicp 139.1', () => + expect( + evaluate( + ` + (def (length items) + (def (lengthiter a count) + (if (isnull a) count + (lengthiter (cdr a) (+ 1 count)))) + (lengthiter items 0)) + (def odds (list 1 3 5 7)) + (length odds) + `, + scope, + ), + ).toEqual(4)); + // append + it('sicp 139.1', () => + expect( + evaluate( + ` +(def (append list1 list2) +(if (isnull list1) + list2 + (cons (car list1) (append (cdr list1) list2)))) + (append squares odds) + `, + scope, + ), + ).toEqual([1, 4, 9, 16, 25, 1, 3, 5, 7])); + // (define (f x y . z) ⟨body⟩) <- tbd: variable argument count + // Mapping over lists + + it('sicp 143.1', () => + expect( + evaluate( + ` +(def (scalelist items factor) (if (isnull items) nil + (cons (* (car items) factor) + (scalelist (cdr items) factor)))) +(scalelist (list 1 2 3 4 5) 10) + `, + scope, + ), + ).toEqual([10, 20, 30, 40, 50])); + + it('sicp 143.1', () => + expect( + evaluate( + ` + (def (map proc items) (if (isnull items) nil + (cons (proc (car items)) + (map proc (cdr items))))) + (map abs (list -10 2.5 -11.6 17)) + `, + scope, + ), + ).toEqual([10, 2.5, 11.6, 17])); + it('sicp 143.1', () => expect(evaluate(`(map (fn (x) (* x x)) (list 1 2 3 4))`, scope)).toEqual([1, 4, 9, 16])); + it('sicp 143.1', () => + expect( + evaluate( + ` +(def (scalelist items factor) (map (fn (x) (* x factor)) items)) +(scalelist (list 1 2 3 4 5) 10) +`, + scope, + ), + ).toEqual([10, 20, 30, 40, 50])); }); From e3a6444f413ee01bb23aef9ac44bb0e69306296e Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 2 May 2025 08:30:09 +0200 Subject: [PATCH 082/174] mondo: support line comments --- packages/mondo/mondo.mjs | 5 ++++- packages/mondo/test/mondo.test.mjs | 8 ++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index bed568661..be7b2a203 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -8,6 +8,7 @@ This program is free software: you can redistribute it and/or modify it under th export class MondoParser { // these are the tokens we expect token_types = { + comment: /^\/\/(.*?)(?=\n|$)/, quotes_double: /^"(.*?)"/, quotes_single: /^'(.*?)'/, open_list: /^\(/, @@ -428,7 +429,9 @@ export class MondoRunner { } evaluate_list(ast, scope) { // evaluate all children before evaluating list (dont mutate!!!) - const args = ast.children.map((arg) => this.evaluate(arg, scope)); + const args = ast.children + .filter((child) => child.type !== 'comment') // ignore comments + .map((arg) => this.evaluate(arg, scope)); const node = { type: 'list', children: args }; return this.evaluator(node, scope); } diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index 4ec18decb..6e9260f08 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -67,6 +67,14 @@ describe('mondo s-expressions parser', () => { { type: 'number', value: '22.3' }, ], })); + it('should parse comments', () => + expect(p('a // hello')).toEqual({ + type: 'list', + children: [ + { type: 'plain', value: 'a' }, + { type: 'comment', value: '// hello' }, + ], + })); }); let desguar = (a) => { From 29e5833c8c48818289b436864af332f15b04cf39 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 2 May 2025 08:32:46 +0200 Subject: [PATCH 083/174] tweak --- packages/mondo/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mondo/README.md b/packages/mondo/README.md index 8bdd93143..b1f504bf9 100644 --- a/packages/mondo/README.md +++ b/packages/mondo/README.md @@ -1,6 +1,6 @@ # mondo -an experimental parser for an *uzulang*, a custom dsl for patterns that can stand on its own feet. more info: +an experimental parser for a maxi notation, a custom dsl for patterns that can stand on its own feet. more info: - [uzulang I](https://garten.salat.dev/uzu/uzulang1.html) - [uzulang II](https://garten.salat.dev/uzu/uzulang2.html) From 60dd8bd763d510197daafe6830dce8c4a3f075ec Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 2 May 2025 08:35:16 +0200 Subject: [PATCH 084/174] tweak again --- packages/mondo/README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/mondo/README.md b/packages/mondo/README.md index b1f504bf9..5233c73d3 100644 --- a/packages/mondo/README.md +++ b/packages/mondo/README.md @@ -1,6 +1,9 @@ # mondo -an experimental parser for a maxi notation, a custom dsl for patterns that can stand on its own feet. more info: +a lisp-based language intended to be used as a custom dsl for patterns that can stand on its own feet. +see the `test` folder for usage examples + +more info: - [uzulang I](https://garten.salat.dev/uzu/uzulang1.html) - [uzulang II](https://garten.salat.dev/uzu/uzulang2.html) From ffe831d24bdbd16d63e30ab8cb67c4b50c4f17d3 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 2 May 2025 08:53:55 +0200 Subject: [PATCH 085/174] mondo: strings now get type "string" to be discernable from plain variables --- packages/mondo/mondo.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index be7b2a203..626793d8e 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -440,7 +440,7 @@ export class MondoRunner { ast.value = Number(ast.value); } else if (['quotes_double', 'quotes_single'].includes(ast.type)) { ast.value = ast.value.slice(1, -1); - ast.type = 'plain'; // is this problematic? + ast.type = 'string'; } return this.evaluator(ast, scope); } From 7565354274bca05ec29156bf084baefdca1be4ad Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 2 May 2025 08:54:34 +0200 Subject: [PATCH 086/174] superdough: fallback to triangle when non-string is given --- packages/superdough/superdough.mjs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 02832b433..8c26910b5 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -101,6 +101,10 @@ export async function aliasBank(...args) { } export function getSound(s) { + if (typeof s !== 'string') { + console.warn(`getSound: expected string got "${s}". fall back to triangle`); + return soundMap.get().triangle; // is this good? + } return soundMap.get()[s.toLowerCase()]; } From 4208c79c0951ef963026b1454a0cc76ed7294555 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 2 May 2025 15:13:04 +0200 Subject: [PATCH 087/174] fix: ! and @ operators --- packages/mondough/mondough.mjs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index e995c0e4e..d995c3d8f 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -49,6 +49,10 @@ function evaluator(node, scope) { if (type === 'list') { const { children } = node; const [name, ...args] = children; + // some functions wont be reified to make sure they work (e.g. see extend below) + if (typeof name === 'function') { + return name(...args); + } if (name.value === 'def') { return silence; } @@ -56,7 +60,7 @@ function evaluator(node, scope) { const first = name.firstCycle(true)[0]; const type = typeof first?.value; if (type !== 'function') { - throw new Error(`[mondough] "${first}" is not a function, got ${type} ...`); + throw new Error(`[mondough] expected function, got "${first?.value}"`); } return name .fmap((fn) => { @@ -73,10 +77,14 @@ function evaluator(node, scope) { return reify(scope[value]); // -> local scope has no location } const variable = lib[value] ?? strudelScope[value]; + // problem: collisions when we want a string that happens to also be a variable name + // example: "s sine" -> sine is also a variable let pat; if (type === 'plain' && typeof variable !== 'undefined') { - // problem: collisions when we want a string that happens to also be a variable name - // example: "s sine" -> sine is also a variable + // some function names are not patternable, so we skip reification here + if (['!', 'extend', '@', 'expand'].includes(value)) { + return variable; + } pat = reify(variable); } else { pat = reify(value); From 607a6121bcd8fc3ebef851b5c6f4fefb25727132 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 2 May 2025 16:20:23 +0200 Subject: [PATCH 088/174] rename mondo package to mondolang --- packages/mondo/README.md | 2 +- packages/mondo/package.json | 2 +- packages/mondough/mondough.mjs | 2 +- packages/mondough/package.json | 3 ++- pnpm-lock.yaml | 3 +++ 5 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/mondo/README.md b/packages/mondo/README.md index 5233c73d3..954f425a9 100644 --- a/packages/mondo/README.md +++ b/packages/mondo/README.md @@ -11,7 +11,7 @@ more info: ## Example Usage ```js -import { MondoRunner } from 'mondo' +import { MondoRunner } from 'mondolang' // define our library of functions and variables let lib = { add: (a, b) => a + b, diff --git a/packages/mondo/package.json b/packages/mondo/package.json index d8a4a0bf2..277bddb1c 100644 --- a/packages/mondo/package.json +++ b/packages/mondo/package.json @@ -1,5 +1,5 @@ { - "name": "mondo", + "name": "mondolang", "version": "1.1.0", "description": "a language for functional composition that translates to js", "main": "mondo.mjs", diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index d995c3d8f..3731e8cee 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -13,7 +13,7 @@ import { silence, } from '@strudel/core'; import { registerLanguage } from '@strudel/transpiler'; -import { MondoRunner } from '../mondo/mondo.mjs'; +import { MondoRunner } from 'mondolang'; const tail = (friend, pat) => pat.fmap((a) => (b) => (Array.isArray(a) ? [...a, b] : [a, b])).appLeft(friend); diff --git a/packages/mondough/package.json b/packages/mondough/package.json index eae22519e..a034424c0 100644 --- a/packages/mondough/package.json +++ b/packages/mondough/package.json @@ -33,7 +33,8 @@ "homepage": "https://github.com/tidalcycles/strudel#readme", "dependencies": { "@strudel/core": "workspace:*", - "@strudel/transpiler": "workspace:*" + "@strudel/transpiler": "workspace:*", + "mondolang": "workspace:*" }, "devDependencies": { "mondo": "*", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 67e5d85d1..7aecd35bd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -364,6 +364,9 @@ importers: '@strudel/transpiler': specifier: workspace:* version: link:../transpiler + mondolang: + specifier: workspace:* + version: link:../mondo devDependencies: mondo: specifier: '*' From 006ce9d1da2af36364d2acce6f4aada71a965766 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Fri, 2 May 2025 23:33:58 -0400 Subject: [PATCH 089/174] delaytime --- packages/superdough/superdough.mjs | 8 ++++---- packages/superdough/util.mjs | 4 ++++ packages/webaudio/webaudio.mjs | 5 +++-- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index a1dc30b7c..2a205e18d 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -7,7 +7,7 @@ This program is free software: you can redistribute it and/or modify it under th import './feedbackdelay.mjs'; import './reverb.mjs'; import './vowel.mjs'; -import { clamp, nanFallback, _mod } from './util.mjs'; +import { clamp, nanFallback, _mod, cycleToSeconds } from './util.mjs'; import workletsUrl from './worklets.mjs?audioworklet'; import { createFilter, gainNode, getCompressor, getWorklet } from './helpers.mjs'; import { map } from 'nanostores'; @@ -132,7 +132,7 @@ const defaultDefaultValues = { distortvol: 1, delay: 0, delayfeedback: 0.5, - delaytime: 0.25, + delaytime: 3 / 16, orbit: 1, i: 1, velocity: 1, @@ -429,7 +429,7 @@ export function resetGlobalEffects() { let activeSoundSources = new Map(); -export const superdough = async (value, t, hapDuration) => { +export const superdough = async (value, t, hapDuration, cps = 1) => { const ac = getAudioContext(); t = typeof t === 'string' && t.startsWith('=') ? Number(t.slice(1)) : ac.currentTime + t; let { stretch } = value; @@ -699,7 +699,7 @@ export const superdough = async (value, t, hapDuration) => { // delay let delaySend; if (delay > 0 && delaytime > 0 && delayfeedback > 0) { - const delyNode = getDelay(orbit, delaytime, delayfeedback, t); + const delyNode = getDelay(orbit, cycleToSeconds(delaytime, cps), delayfeedback, t); delaySend = effectSend(post, delyNode, delay); audioNodes.push(delaySend); } diff --git a/packages/superdough/util.mjs b/packages/superdough/util.mjs index 4e3c7e41e..ea63a16f0 100644 --- a/packages/superdough/util.mjs +++ b/packages/superdough/util.mjs @@ -68,3 +68,7 @@ export const _mod = (n, m) => ((n % m) + m) % m; export const getSoundIndex = (n, numSounds) => { return _mod(Math.round(nanFallback(n, 0)), numSounds); }; + +export function cycleToSeconds(cycle, cps) { + return cycle / cps; +} diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 44a683480..3ce4f91b3 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -17,8 +17,9 @@ const hap2value = (hap) => { export const webaudioOutputTrigger = (t, hap, ct, cps) => superdough(hap2value(hap), t - ct, hap.duration / cps, cps); // uses more precise, absolute t if available, see https://github.com/tidalcycles/strudel/pull/1004 -export const webaudioOutput = (hap, deadline, hapDuration, cps, t) => - superdough(hap2value(hap), t ? `=${t}` : deadline, hapDuration); +export const webaudioOutput = (hap, deadline, hapDuration, cps, t) => { + return superdough(hap2value(hap), t ? `=${t}` : deadline, hapDuration,cps); +}; Pattern.prototype.webaudio = function () { return this.onTrigger(webaudioOutputTrigger); From 94257e81ee6c43053ffa739eff88c1454ba36604 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Fri, 2 May 2025 23:44:55 -0400 Subject: [PATCH 090/174] lint --- packages/webaudio/webaudio.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 3ce4f91b3..640449db0 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -18,7 +18,7 @@ const hap2value = (hap) => { export const webaudioOutputTrigger = (t, hap, ct, cps) => superdough(hap2value(hap), t - ct, hap.duration / cps, cps); // uses more precise, absolute t if available, see https://github.com/tidalcycles/strudel/pull/1004 export const webaudioOutput = (hap, deadline, hapDuration, cps, t) => { - return superdough(hap2value(hap), t ? `=${t}` : deadline, hapDuration,cps); + return superdough(hap2value(hap), t ? `=${t}` : deadline, hapDuration, cps); }; Pattern.prototype.webaudio = function () { From 2951a60fac778ee1b7a8b04d51282957425d7758 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 3 May 2025 11:44:02 +0200 Subject: [PATCH 091/174] fix: rests at the end --- packages/mondo/mondo.mjs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 626793d8e..6ec63c8d0 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -155,7 +155,9 @@ export class MondoParser { if (opIndex === -1) break; const op = { type: 'plain', value: children[opIndex].value }; if (opIndex === children.length - 1) { - throw new Error(`cannot use operator as last child.`); + //throw new Error(`cannot use operator as last child.`); + children[opIndex] = op; // ignore operator if last child.. e.g. "note [c -]" + continue; } if (opIndex === 0) { // regular function call (assuming each operator exists as function) From 9b10dc85351a2ef23e71d708b0595b69132aeb10 Mon Sep 17 00:00:00 2001 From: alex Date: Sat, 3 May 2025 20:37:44 +0100 Subject: [PATCH 092/174] change pipe symbol from '.' to '#' --- packages/mondo/mondo.mjs | 7 +++---- packages/mondo/test/mondo.test.mjs | 28 ++++++++++++++-------------- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 6ec63c8d0..fe48509c5 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -22,7 +22,7 @@ export class MondoParser { number: /^-?[0-9]*\.?[0-9]+/, // before pipe! op: /^[*/:!@%?+-]|^\.{2}/, // * / : ! @ % ? .. // dollar: /^\$/, - pipe: /^\./, + pipe: /^\#/, stack: /^[,$]/, or: /^[|]/, plain: /^[a-zA-Z0-9-~_^#]+/, @@ -309,9 +309,8 @@ export function printAst(ast, compact = false, lvl = 0) { const br = compact ? '' : '\n'; const spaces = compact ? '' : Array(lvl).fill(' ').join(''); if (ast.type === 'list') { - return `${lvl ? br : ''}${spaces}(${ast.children.map((child) => printAst(child, compact, lvl + 1)).join(' ')}${ - ast.children.find((child) => child.type === 'list') ? `${br}${spaces})` : ')' - }`; + return `${lvl ? br : ''}${spaces}(${ast.children.map((child) => printAst(child, compact, lvl + 1)).join(' ')}${ast.children.find((child) => child.type === 'list') ? `${br}${spaces})` : ')' + }`; } return `${ast.value}`; } diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index 6e9260f08..41558aaab 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -89,13 +89,13 @@ describe('mondo sugar', () => { it('should desugar mixed [] <>', () => expect(desguar('[a ]')).toEqual('(square a (angle b c))')); it('should desugar mixed <> []', () => expect(desguar('')).toEqual('(angle a (square b c))')); - it('should desugar .', () => expect(desguar('s jazz . fast 2')).toEqual('(fast 2 (s jazz))')); - it('should desugar . square', () => expect(desguar('[bd cp . fast 2]')).toEqual('(fast 2 (square bd cp))')); - it('should desugar . twice', () => expect(desguar('s jazz . fast 2 . slow 2')).toEqual('(slow 2 (fast 2 (s jazz)))')); - it('should desugar . nested', () => expect(desguar('(s cp . fast 2)')).toEqual('(fast 2 (s cp))')); - it('should desugar . within []', () => expect(desguar('[bd cp . fast 2]')).toEqual('(fast 2 (square bd cp))')); - it('should desugar . within , within []', () => - expect(desguar('[bd cp . fast 2, x]')).toEqual('(stack (fast 2 (square bd cp)) x)')); + it('should desugar #', () => expect(desguar('s jazz # fast 2')).toEqual('(fast 2 (s jazz))')); + it('should desugar # square', () => expect(desguar('[bd cp # fast 2]')).toEqual('(fast 2 (square bd cp))')); + it('should desugar # twice', () => expect(desguar('s jazz # fast 2 # slow 2')).toEqual('(slow 2 (fast 2 (s jazz)))')); + it('should desugar # nested', () => expect(desguar('(s cp # fast 2)')).toEqual('(fast 2 (s cp))')); + it('should desugar # within []', () => expect(desguar('[bd cp # fast 2]')).toEqual('(fast 2 (square bd cp))')); + it('should desugar # within , within []', () => + expect(desguar('[bd cp # fast 2, x]')).toEqual('(stack (fast 2 (square bd cp)) x)')); // it('should desugar .(.', () => expect(desguar('[jazz hh.(.fast 2)]')).toEqual('(square jazz (fast 2 hh))')); @@ -123,15 +123,15 @@ describe('mondo sugar', () => { it('should desugar x $ y . z', () => expect(desguar('x $ y . z')).toEqual('(z (x y))')); */ it('should desugar README example', () => - expect(desguar('s [bd hh*2 (cp.crush 4) ] . speed .8')).toEqual( + expect(desguar('s [bd hh*2 (cp # crush 4) ] # speed .8')).toEqual( '(speed .8 (s (square bd (* 2 hh) (crush 4 cp) (angle mt ht lt))))', )); - it('should desugar (.)', () => expect(desguar('(.)')).toEqual('(fn (_) _)')); - it('should desugar lambda', () => expect(desguar('(.fast 2)')).toEqual('(fn (_) (fast 2 _))')); - it('should desugar lambda call', () => expect(desguar('((.mul 2) 2)')).toEqual('((fn (_) (mul 2 _)) 2)')); + it('should desugar (#)', () => expect(desguar('(#)')).toEqual('(fn (_) _)')); + it('should desugar lambda', () => expect(desguar('(# fast 2)')).toEqual('(fn (_) (fast 2 _))')); + it('should desugar lambda call', () => expect(desguar('((# mul 2) 2)')).toEqual('((fn (_) (mul 2 _)) 2)')); it('should desugar lambda with pipe', () => - expect(desguar('(.fast 2 .room 1)')).toEqual('(fn (_) (room 1 (fast 2 _)))')); + expect(desguar('(# fast 2 # room 1)')).toEqual('(fn (_) (room 1 (fast 2 _)))')); /* const lambda = parser.parse('(lambda (_) (fast 2 _))'); const target = { type: 'plain', value: 'xyz' }; it('should desugar_lambda', () => @@ -141,8 +141,8 @@ describe('mondo sugar', () => { describe('mondo arithmetic', () => { let multi = (op) => - (init, ...rest) => - rest.reduce((acc, arg) => op(acc, arg), init); + (init, ...rest) => + rest.reduce((acc, arg) => op(acc, arg), init); let lib = { '+': multi((a, b) => a + b), From 1eb5f6a99505d6db5c72909ab4559c4b695597b7 Mon Sep 17 00:00:00 2001 From: alex Date: Sat, 3 May 2025 20:38:43 +0100 Subject: [PATCH 093/174] format --- packages/mondo/mondo.mjs | 5 +- packages/mondo/test/mondo.test.mjs | 4 +- website/src/pages/learn/mondo-notation.mdx | 67 ++++++++++++---------- 3 files changed, 41 insertions(+), 35 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index fe48509c5..142b856dc 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -309,8 +309,9 @@ export function printAst(ast, compact = false, lvl = 0) { const br = compact ? '' : '\n'; const spaces = compact ? '' : Array(lvl).fill(' ').join(''); if (ast.type === 'list') { - return `${lvl ? br : ''}${spaces}(${ast.children.map((child) => printAst(child, compact, lvl + 1)).join(' ')}${ast.children.find((child) => child.type === 'list') ? `${br}${spaces})` : ')' - }`; + return `${lvl ? br : ''}${spaces}(${ast.children.map((child) => printAst(child, compact, lvl + 1)).join(' ')}${ + ast.children.find((child) => child.type === 'list') ? `${br}${spaces})` : ')' + }`; } return `${ast.value}`; } diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index 41558aaab..6393b10f9 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -141,8 +141,8 @@ describe('mondo sugar', () => { describe('mondo arithmetic', () => { let multi = (op) => - (init, ...rest) => - rest.reduce((acc, arg) => op(acc, arg), init); + (init, ...rest) => + rest.reduce((acc, arg) => op(acc, arg), init); let lib = { '+': multi((a, b) => a + b), diff --git a/website/src/pages/learn/mondo-notation.mdx b/website/src/pages/learn/mondo-notation.mdx index 2bc952341..ccf5b72b6 100644 --- a/website/src/pages/learn/mondo-notation.mdx +++ b/website/src/pages/learn/mondo-notation.mdx @@ -14,20 +14,25 @@ Here's an example: <8 16>) . *2 -.s "sine" .add (note [0 <12 24>]*2) -.dec(sine .range .2 2) .room .5 -.lpf(sine/3.range 120 400) -.lpenv(rand .range .5 4) -.lpq(perlin .range 5 12 . * 2) -.dist 1 .fm 4 .fmh 5.01 .fmdecay <.1 .2> -.postgain .6 .delay .1 .clip 5 + tune={`$ note (c2 # euclid <3 6 3> <8 16>) # *2 + # s "sine" # add (note [0 <12 24>]*2) + # dec(sine # range .2 2) + # room .5 + # lpf (sine/3 # range 120 400) + # lpenv (rand # range .5 4) + # lpq (perlin # range 5 12 # * 2) + # dist 1 # fm 4 # fmh 5.01 # fmdecay <.1 .2> + # postgain .6 # delay .1 # clip 5 -$ s [bd bd bd bd] .bank tr909.clip.5 -.ply<1 [1 [2 4]]> +$ s [bd bd bd bd] # bank tr909 # clip .5 -$ s oh*4 .press .bank tr909 .speed.8 -.dec (<.02 .05>*2 .add (saw/8.range 0 1))`} +# ply <1 [1 [2 4]]> + +$ s oh\*4 # press # bank tr909 # speed.8 + +# dec (<.02 .05>\*2 # add (saw/8 # range 0 1)) + +`} /> ## Mondo in the REPL @@ -95,16 +100,16 @@ Besides function calling with round parens, Mondo Notation has a lot in common w ## Chaining Functions -Similar to how it works in JS, we can chain functions calls with the "." operator: +Similar to how "." works in javascript (JS), we can chain functions calls with the "#" operator: *4 -.scale C4:minor -.jux rev -.dec .2 -.delay .5`} + # scale C4:minor + # jux rev + # dec .2 + # delay .5`} /> Here's the same written in JS: @@ -112,29 +117,29 @@ Here's the same written in JS: *4") -.scale("C4:minor") -.jux(rev) -.dec(.2) -.delay(.5)`} + # scale("C4:minor") + # jux(rev) + # dec(.2) + # delay(.5)`} /> ### Chaining Functions Locally -A function can be applied "locally" by wrapping it in round parens: +A function can be applied to a single element by wrapping it in round parens: - + in this case, `delay .6` will only be applied to `cp`. compare this with the JS version: -here we can see much we can save when there's no boundary between mini notation and function calls! +here we can see how much we can save when there's no boundary between mini notation and function calls! ### Chaining Infix Operators Infix operators exist as regular functions, so they can be chained as well: - + In this case, the \*2 will be applied to the whole pattern. @@ -146,17 +151,17 @@ Some functions in strudel expect a function as input, for example: in mondo, the `x=>x.` can be shortened to: - + chaining works as expected: - + ## Strings You can use "double quotes" and 'single quotes' to get a string: - + ## Multiple Patterns @@ -165,9 +170,9 @@ The `$` sign can be used to separate multiple patterns: .voicing -.struct[x - - x - x - -].delay.5`} + tune={`$ s [bd rim [~ bd] rim] # bank tr707 +$ chord # voicing + # struct[x ~ ~ x ~ x ~ ~] # delay .5`} /> The `$` sign is an alias for `,` so it will create a stack behind the scenes. From 98efd72ab496231f9d67e1bfc6aa60976f2373fb Mon Sep 17 00:00:00 2001 From: alex Date: Sat, 3 May 2025 20:59:05 +0100 Subject: [PATCH 094/174] delint --- packages/mondo/mondo.mjs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 142b856dc..0f9da0eb9 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -22,7 +22,7 @@ export class MondoParser { number: /^-?[0-9]*\.?[0-9]+/, // before pipe! op: /^[*/:!@%?+-]|^\.{2}/, // * / : ! @ % ? .. // dollar: /^\$/, - pipe: /^\#/, + pipe: /^#/, stack: /^[,$]/, or: /^[|]/, plain: /^[a-zA-Z0-9-~_^#]+/, @@ -309,9 +309,8 @@ export function printAst(ast, compact = false, lvl = 0) { const br = compact ? '' : '\n'; const spaces = compact ? '' : Array(lvl).fill(' ').join(''); if (ast.type === 'list') { - return `${lvl ? br : ''}${spaces}(${ast.children.map((child) => printAst(child, compact, lvl + 1)).join(' ')}${ - ast.children.find((child) => child.type === 'list') ? `${br}${spaces})` : ')' - }`; + return `${lvl ? br : ''}${spaces}(${ast.children.map((child) => printAst(child, compact, lvl + 1)).join(' ')}${ast.children.find((child) => child.type === 'list') ? `${br}${spaces})` : ')' + }`; } return `${ast.value}`; } From 1b3b07842ec86ac65715803d3776fe5b6dc6f300 Mon Sep 17 00:00:00 2001 From: alex Date: Sat, 3 May 2025 21:57:01 +0100 Subject: [PATCH 095/174] format again.. --- packages/mondo/mondo.mjs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 0f9da0eb9..73dbf1c7a 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -309,8 +309,9 @@ export function printAst(ast, compact = false, lvl = 0) { const br = compact ? '' : '\n'; const spaces = compact ? '' : Array(lvl).fill(' ').join(''); if (ast.type === 'list') { - return `${lvl ? br : ''}${spaces}(${ast.children.map((child) => printAst(child, compact, lvl + 1)).join(' ')}${ast.children.find((child) => child.type === 'list') ? `${br}${spaces})` : ')' - }`; + return `${lvl ? br : ''}${spaces}(${ast.children.map((child) => printAst(child, compact, lvl + 1)).join(' ')}${ + ast.children.find((child) => child.type === 'list') ? `${br}${spaces})` : ')' + }`; } return `${ast.value}`; } From 2c9f9d785f13e76d5c7304372d0e8c9f4df615d1 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 12 Jun 2025 08:39:27 +0200 Subject: [PATCH 096/174] hotfix: close bakery --- website/src/config.ts | 2 +- website/src/repl/components/Header.jsx | 8 ++-- .../src/repl/components/panel/PatternsTab.jsx | 37 +++++++++++-------- 3 files changed, 26 insertions(+), 21 deletions(-) diff --git a/website/src/config.ts b/website/src/config.ts index 2f499d55f..e88b7f3e1 100644 --- a/website/src/config.ts +++ b/website/src/config.ts @@ -58,7 +58,7 @@ export const SIDEBAR: Sidebar = { { text: 'What is Strudel?', link: 'workshop/getting-started' }, { text: 'Showcase', link: 'intro/showcase' }, { text: 'Blog', link: 'blog' }, - { text: 'Community Bakery', link: 'bakery' }, + /* { text: 'Community Bakery', link: 'bakery' }, */ ], Workshop: [ // { text: 'Getting Started', link: 'workshop/getting-started' }, diff --git a/website/src/repl/components/Header.jsx b/website/src/repl/components/Header.jsx index 2963a5f97..a97e2bed0 100644 --- a/website/src/repl/components/Header.jsx +++ b/website/src/repl/components/Header.jsx @@ -93,7 +93,7 @@ export function Header({ context, embedded = false }) { > {!isEmbedded && update} - {!isEmbedded && ( + {/* !isEmbedded && ( - )} - {!isEmbedded && ( + ) */} + {/* !isEmbedded && ( - )} + ) */} {!isEmbedded && (
- {patternFilter === patternFilterName.user && ( - - updateCodeWindow( - context, - { ...userPatterns[id], collection: userPattern.collection }, - autoResetPatternOnChange, - ) - } - patterns={userPatterns} - started={context.started} - activePattern={activePattern} - viewingPatternID={viewingPatternID} - /> - )} + {/* {patternFilter === patternFilterName.user && ( */} + + updateCodeWindow( + context, + { ...userPatterns[id], collection: userPattern.collection }, + autoResetPatternOnChange, + ) + } + patterns={userPatterns} + started={context.started} + activePattern={activePattern} + viewingPatternID={viewingPatternID} + /> + {/* )} */}
); @@ -250,6 +250,11 @@ export function PatternsTab({ context }) { const { patternFilter } = useSettings(); return ( +
+ +
+ ); + /* return (
)}
- ); + ); */ } From 79f7938b3381b426e85b67ec163abaee5423ed92 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 12 Jun 2025 08:51:03 +0200 Subject: [PATCH 097/174] hotfix: add default code --- website/src/repl/useReplContext.jsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/website/src/repl/useReplContext.jsx b/website/src/repl/useReplContext.jsx index f88e5a6e2..708506cc5 100644 --- a/website/src/repl/useReplContext.jsx +++ b/website/src/repl/useReplContext.jsx @@ -59,6 +59,8 @@ async function getModule(name) { return modules.find((m) => m.packageName === name); } +const initialCode = `$: s("[bd ]*2").bank("tr909").dec(.4)`; + export function useReplContext() { const { isSyncEnabled, audioEngineTarget } = useSettings(); const shouldUseWebaudio = audioEngineTarget !== audioEngineTargets.osc; @@ -77,7 +79,7 @@ export function useReplContext() { transpiler, autodraw: false, root: containerRef.current, - initialCode: '// LOADING', + initialCode, pattern: silence, drawTime, drawContext, From 40ba5d44652d18dc84bcc4d9f9d542f18d573551 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 12 Jun 2025 09:11:19 +0200 Subject: [PATCH 098/174] hotfix: don't load patterns from db --- website/src/repl/util.mjs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/website/src/repl/util.mjs b/website/src/repl/util.mjs index f49dd568f..9db280dad 100644 --- a/website/src/repl/util.mjs +++ b/website/src/repl/util.mjs @@ -7,7 +7,7 @@ import './Repl.css'; import { createClient } from '@supabase/supabase-js'; import { nanoid } from 'nanoid'; import { writeText } from '@tauri-apps/plugin-clipboard-manager'; -import { $featuredPatterns, loadDBPatterns } from '@src/user_pattern_utils.mjs'; +import { $featuredPatterns /* , loadDBPatterns */ } from '@src/user_pattern_utils.mjs'; // Create a single supabase client for interacting with your database export const supabase = createClient( @@ -16,9 +16,9 @@ export const supabase = createClient( ); let dbLoaded; -if (typeof window !== 'undefined') { +/* if (typeof window !== 'undefined') { dbLoaded = loadDBPatterns(); -} +} */ export async function initCode() { // load code from url hash (either short hash from database or decode long hash) From ef3b9a68e362218bef0a79f0954fad5fa6a38aae Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 12 Jun 2025 09:21:57 +0200 Subject: [PATCH 099/174] hotfix: leave out part about shuffle in welcome tab --- website/src/repl/components/panel/WelcomeTab.jsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/website/src/repl/components/panel/WelcomeTab.jsx b/website/src/repl/components/panel/WelcomeTab.jsx index 1049a581d..a43f02ad4 100644 --- a/website/src/repl/components/panel/WelcomeTab.jsx +++ b/website/src/repl/components/panel/WelcomeTab.jsx @@ -15,17 +15,18 @@ export function WelcomeTab({ context }) {
1. hit play - 2. change something -{' '} 3. hit update -
- If you don't like what you hear, try shuffle! + {/*
+ If you don't like what you hear, try shuffle! */}

- To learn more about what this all means, check out the{' '} + {/* To learn more about what this all means, check out the{' '} */} + To get started, check out the interactive tutorial . Also feel free to join the{' '} - tidalcycles discord channel + discord channel {' '} to ask any questions, give feedback or just say hello.

From 694a22af85494444c3d07c3c48da48d20afbe390 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 12 Jun 2025 09:51:04 +0200 Subject: [PATCH 100/174] hotfix: bring back loading code + fill in default pattern later --- website/src/repl/useReplContext.jsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/website/src/repl/useReplContext.jsx b/website/src/repl/useReplContext.jsx index 708506cc5..36e8099cb 100644 --- a/website/src/repl/useReplContext.jsx +++ b/website/src/repl/useReplContext.jsx @@ -59,7 +59,7 @@ async function getModule(name) { return modules.find((m) => m.packageName === name); } -const initialCode = `$: s("[bd ]*2").bank("tr909").dec(.4)`; +const initialCode = `// LOADING`; export function useReplContext() { const { isSyncEnabled, audioEngineTarget } = useSettings(); @@ -135,9 +135,10 @@ export function useReplContext() { code = latestCode; msg = `Your last session has been loaded!`; } else { - const { code: randomTune, name } = await getRandomTune(); - code = randomTune; - msg = `A random code snippet named "${name}" has been loaded!`; + /* const { code: randomTune, name } = await getRandomTune(); + code = randomTune; */ + code = '$: s("[bd ]*2").bank("tr909").dec(.4)'; + msg = `Default code has been loaded`; } editor.setCode(code); setDocumentTitle(code); From 372542dcd86f7f201d6b187c875be4b2032f972a Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Thu, 12 Jun 2025 10:23:21 +0200 Subject: [PATCH 101/174] working --- website/src/repl/components/Header.jsx | 4 +- website/src/repl/util.mjs | 76 ++++++++++++++++---------- 2 files changed, 49 insertions(+), 31 deletions(-) diff --git a/website/src/repl/components/Header.jsx b/website/src/repl/components/Header.jsx index a97e2bed0..ca4e1ecf9 100644 --- a/website/src/repl/components/Header.jsx +++ b/website/src/repl/components/Header.jsx @@ -102,7 +102,7 @@ export function Header({ context, embedded = false }) { shuffle ) */} - {/* !isEmbedded && ( + {!isEmbedded && ( - ) */} + )} {!isEmbedded && ( { - const hash = nanoid(12); - const shareUrl = window.location.origin + window.location.pathname + '?' + hash; - const { error } = await supabase.from('code_v1').insert([{ code: codeToShare, hash, ['public']: isPublic }]); - if (!error) { - lastShared = codeToShare; - // copy shareUrl to clipboard - if (isTauri()) { - await writeText(shareUrl); - } else { - await navigator.clipboard.writeText(shareUrl); - } - const message = `Link copied to clipboard: ${shareUrl}`; - alert(message); - // alert(message); - logger(message, 'highlight'); +//RIP due to SPAM +// export async function shareCode(codeToShare) { +// // const codeToShare = activeCode || code; +// if (lastShared === codeToShare) { +// logger(`Link already generated!`, 'error'); +// return; +// } + +// confirmDialog( +// 'Do you want your pattern to be public? If no, press cancel and you will get just a private link.', +// ).then(async (isPublic) => { +// const hash = nanoid(12); +// const shareUrl = window.location.origin + window.location.pathname + '?' + hash; +// const { error } = await supabase.from('code_v1').insert([{ code: codeToShare, hash, ['public']: isPublic }]); +// if (!error) { +// lastShared = codeToShare; +// // copy shareUrl to clipboard +// if (isTauri()) { +// await writeText(shareUrl); +// } else { +// await navigator.clipboard.writeText(shareUrl); +// } +// const message = `Link copied to clipboard: ${shareUrl}`; +// alert(message); +// // alert(message); +// logger(message, 'highlight'); +// } else { +// console.log('error', error); +// const message = `Error: ${error.message}`; +// // alert(message); +// logger(message); +// } +// }); +// } + +export async function shareCode() { + try { + const shareUrl = window.location.href; + if (isTauri()) { + await writeText(shareUrl); } else { - console.log('error', error); - const message = `Error: ${error.message}`; - // alert(message); - logger(message); + await navigator.clipboard.writeText(shareUrl); } - }); + const message = `Link copied to clipboard!`; + alert(message); + logger(message, 'highlight'); + } catch (e) { + console.error(e); + } } export const isIframe = () => window.location !== window.parent.location; From 94746b5fae73f34b287ca1c9ae20e65a7ff0202f Mon Sep 17 00:00:00 2001 From: Alex McLean Date: Thu, 12 Jun 2025 13:37:58 +0100 Subject: [PATCH 102/174] Update README.md --- README.md | 53 +---------------------------------------------------- 1 file changed, 1 insertion(+), 52 deletions(-) diff --git a/README.md b/README.md index 12ee85035..0d576ac6d 100644 --- a/README.md +++ b/README.md @@ -1,54 +1,3 @@ # strudel -[![Strudel test status](https://github.com/tidalcycles/strudel/actions/workflows/test.yml/badge.svg)](https://github.com/tidalcycles/strudel/actions) [![DOI](https://zenodo.org/badge/450927247.svg)](https://doi.org/10.5281/zenodo.6659278) - -An experiment in making a [Tidal](https://github.com/tidalcycles/tidal/) using web technologies. This software is a bit more stable now, but please continue to tread carefully. - -- Try it here: -- Docs: -- Technical Blog Post: -- 1 Year of Strudel Blog Post: -- 2 Years of Strudel Blog Post: - -## Running Locally - -After cloning the project, you can run the REPL locally: - -1. Install [Node.js](https://nodejs.org/) -2. Install [pnpm](https://pnpm.io/installation) -3. Install dependencies by running the following command: - ```bash - pnpm i - ``` -4. Run the development server: - ```bash - pnpm dev - ``` - -## Using Strudel In Your Project - -This project is organized into many [packages](./packages), which are also available on [npm](https://www.npmjs.com/search?q=%40strudel). - -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). - - - - - -Made with [contrib.rocks](https://contrib.rocks). - -## Community - -There is a #strudel channel on the TidalCycles discord: - -You can also ask questions and find related discussions on the tidal club forum: - -The discord and forum is shared with the haskell (tidal) and python (vortex) siblings of this project. +Moving to https://codeberg.org/uzu/strudel From 84efa664caa4c78a0675326d6f76894d01e3310c Mon Sep 17 00:00:00 2001 From: Alex McLean Date: Thu, 12 Jun 2025 13:39:00 +0100 Subject: [PATCH 103/174] Update README.md --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0d576ac6d..792245236 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,8 @@ # strudel -Moving to https://codeberg.org/uzu/strudel +Live coding patterns on the web +https://strudel.cc/ + +Development is moving to https://codeberg.org/uzu/strudel + +Please update your bookmarks. From b6e67f34743fee69a92b0d98ba9996d5409d1821 Mon Sep 17 00:00:00 2001 From: Alex McLean Date: Thu, 12 Jun 2025 14:12:42 +0100 Subject: [PATCH 104/174] > codeberg --- CONTRIBUTING.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7d8170cfc..618830b9c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,11 +2,14 @@ Thanks for wanting to contribute!!! There are many ways you can add value to this project +## Move to codeberg + +We are currently in the process of moving from github to codeberg -- not everything is working, please bear with us. + ## Communication Channels To get in touch with the contributors, either -- open a [github discussion](https://github.com/tidalcycles/strudel/discussions) or - [join the Tidal Discord Channel](https://discord.gg/remJ6gQA) and go to the #strudel channel - Find related discussions on the [tidal club forum](https://club.tidalcycles.org/) @@ -32,7 +35,7 @@ Use one of the Communication Channels listed above. ## Improve the Docs If you find some weak spots in the [docs](https://strudel.cc/workshop/getting-started/), -you can edit each file directly on github via the "Edit this page" link located in the right sidebar. +you can edit each file directly on codeburg. (we are currently fixing the "Edit this page" links in the right sidebar) ## Propose a Feature @@ -41,7 +44,7 @@ Maybe you even want to help with the implementation of that feature! ## Report a Bug -If you've found a bug, or some behaviour that does not seem right, you are welcome to file an [issue](https://github.com/tidalcycles/strudel/issues). +If you've found a bug, or some behaviour that does not seem right, you are welcome to file an [issue](https://codeberg.org/uzu/strudel/issues). Please check that it has not been reported before. ## Fix a Bug @@ -71,7 +74,7 @@ To get the project up and running for development, make sure you have installed: then, do the following: ```sh -git clone https://github.com/tidalcycles/strudel.git && cd strudel +git clone https://codeberg.org/uzu/strudel.git && cd strudel pnpm i # install at root to symlink packages pnpm start # start repl ``` @@ -113,7 +116,7 @@ You can run the same check with `pnpm check` ## Package Workflow -The project is split into multiple [packages](https://github.com/tidalcycles/strudel/tree/main/packages) with independent versioning. +The project is split into multiple [packages](https://codeberg.org/uzu/strudel/src/branch/main/packages) with independent versioning. When you run `pnpm i` on the root folder, [pnpm workspaces](https://pnpm.io/workspaces) will install all dependencies of all subpackages. This will allow any js file to import `@strudel/` to get the local version, allowing to develop multiple packages at the same time. From 580447d00af888513357b2a8f8ee74acace1fbb6 Mon Sep 17 00:00:00 2001 From: Alex McLean Date: Thu, 12 Jun 2025 14:15:07 +0100 Subject: [PATCH 105/174] move technical manual from https://github.com/tidalcycles/strudel/wiki/Technical-Manual --- technical.manual.md | 193 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 technical.manual.md diff --git a/technical.manual.md b/technical.manual.md new file mode 100644 index 000000000..73180b145 --- /dev/null +++ b/technical.manual.md @@ -0,0 +1,193 @@ +This document introduces you to Strudel in a technical sense. If you just want to *use* Strudel, have a look at the [Tutorial](https://strudel.tidalcycles.org/tutorial/). + +## Strudel Packages + +There are different packages for different purposes. They.. + +- split up the code into smaller chunks +- can be selectively used to implement some sort of time based system + +Please refer to the individual README files in the [packages folder](https://github.com/tidalcycles/strudel/tree/main/packages) + +## REPL + +The [REPL](https://strudel.tidalcycles.org/) is the place where all packages come together to form a live coding system. It can also be seen as a reference implementation for users of the library. + +More info in the [REPL README](https://github.com/tidalcycles/strudel/tree/main/repl) + +# High Level Overview + + + +## 1. End User Code + +The End User Code is written in JavaScript with added syntax sugar. The [eval package](https://github.com/tidalcycles/strudel/tree/main/packages/eval#strudelcycleseval) evaluates the user code +after a transpilation step, which resolves the syntax sugar. If you don't want the syntax sugar, you can omit the eval package and call the native javascript `eval` instead. + +### 🍭 Syntax Sugar + +JavaScript Transpilation = converting valid JavaScript to valid JavaScript: + +```js +"c3 [e3 g3]".fast(2) +``` + +becomes + +```js +mini('c3 [e3 g3]') + .withMiniLocation([1, 0, 0], [1, 11, 11]) // source location + .fast(2); +``` + +- double quoted strings and backtick strings are turned into `mini` calls (single quoted strings are left as is) +- The source location is added by chaining `withMiniLocation`, which enables the real time highlighting later +- (psuedo) variable names that look like notes (like `c4`, `bb2` or `fs3`) are turned into strings +- support for top level await +- operator overloading could be implemented in the future + +This is how it works: + + + +- The user code is parsed with a [shift parser](https://github.com/shapesecurity/shift-parser-js), generating an AST +- The AST is transformed to resolve the syntax sugar +- The AST is used to generate code again (shift-codegen) + +Shift will most likely be replaced with acorn in the future, see https://github.com/tidalcycles/strudel/issues/174 + +### Mini Notation + +Another important part of the user code is the mini notation, which allows to express rhythms in a short manner. + +- the mini notation is [implemented as a PEG grammar](https://github.com/tidalcycles/strudel/blob/main/packages/mini/krill.pegjs), living in the [mini package](https://github.com/tidalcycles/strudel/tree/main/packages/mini) +- it is based on [krill](https://github.com/Mdashdotdashn/krill) by Mdashdotdashn +- the peg grammar is used to generate a parser with [peggyjs](https://peggyjs.org/) +- the generated parser takes a mini notation string and outputs an AST +- the AST can then be used to construct a pattern using the regular Strudel API + +Here's an example AST: + +```json +{ + "type_": "pattern", + "arguments_": { "alignment": "h" }, + "source_": [ + { + "type_": "element", "source_": "c3", + "location_": { "start": { "offset": 1, "line": 1, "column": 2 }, "end": { "offset": 4, "line": 1, "column": 5 } } + }, + { + "type_": "element", + "location_": { "start": { "offset": 4, "line": 1, "column": 5 }, "end": { "offset": 11, "line": 1, "column": 12 } } + "source_": { + "type_": "pattern", "arguments_": { "alignment": "h" }, + "source_": [ + { + "type_": "element", "source_": "e3", + "location_": { "start": { "offset": 5, "line": 1, "column": 6 }, "end": { "offset": 8, "line": 1, "column": 9 } } + }, + { + "type_": "element", "source_": "g3", + "location_": { "start": { "offset": 8, "line": 1, "column": 9 }, "end": { "offset": 10, "line": 1, "column": 11 } } + } + ] + }, + } + ] +} +``` + +which translates to `seq(c3, seq(e3, g3))` + +## 2. Querying & Scheduling + +When the user code has been evaluated, we hopefully get a Pattern instance, which we can use to query events from. +These events can then be used to trigger side effects in the real world. On that note, Events are mostly called Hap(s) in the codebase, because JS already has a built in `Event` class. + +### Querying + +> Querying = Asking a Pattern for Events within a certain time span + +```js +seq('c3', ['e3', 'g3']) // <--- Pattern + .queryArc(0, 2) // query events within 0 and 2 cycles + .map((hap) => hap.showWhole()); // make readable +``` + +yields + +```js +[ + '0/1 -> 1/2: c3', // cycle 0 + '1/2 -> 3/4: e3', + '3/4 -> 1/1: g3', + '1/1 -> 3/2: c3', // cycle 1 + '3/2 -> 7/4: e3', + '7/4 -> 2/1: g3', +]; +``` + +### 🗓️ Scheduling + +The scheduler will query events repeatedly, creating a possibly endless loop of time slices. +Here is a simplified example of how it works + +```js +let step = 0.5; // query interval in seconds +let tick = 0; // how many intervals have passed +let pattern = seq('c3', ['e3', 'g3']); // pattern from user +setInterval(() => { + const events = pattern.queryArc(tick * step, ++tick * step); + events.forEach((event) => { + console.log(event.showWhole()); + const o = getAudioContext().createOscillator(); + o.frequency.value = getFreq(event.value); + o.start(event.whole.begin); + o.stop(event.whole.begin + event.duration); + o.connect(getAudioContext().destination); + }); +}, step * 1000); // query each "step" seconds +``` + +## 3. Sound Output + +The third and last step is to use the scheduled events to make sound. +Patterns are wrapped with param functions to compose different properties of the sound. + +```js +note("[c2(3,8) [ bb1]]") // sets frequency + .s("") // sound source + .gain(.5) // turn down volume + .cutoff(sine.range(200,1000).slow(4)) // modulated cutoff + .slow(2) + .out().logValues()`, + ]} +/> +``` + +Here is an example Hap value with different properties: + +```js +{ note: 'a4', s: 'sawtooth', gain: 0.5, cutoff: 267 } +``` + + +
+ +- Patterns represent just values in time! +- Suitable for any time based output (music, visuals, movement, .. ?) + +### Supported Outputs + +At the time of writing this doc, the following outputs are supported: + +- Web Audio API `.out()` see [/webaudio](https://github.com/tidalcycles/strudel/tree/main/packages/webaudio) +- MIDI `.midi()` see [/midi](https://github.com/tidalcycles/strudel/tree/main/packages/midi) +- OSC `.osc()` see [/osc](https://github.com/tidalcycles/strudel/tree/main/packages/osc) +- Serial `.serial()` see [/serial](https://github.com/tidalcycles/strudel/tree/main/packages/serial) +- Tone.js `.tone()` (deprecated?) [/tone](https://github.com/tidalcycles/strudel/tree/main/packages/tone) +- WebDirt `.webdirt()` (deprecated?) [/webdirt](https://github.com/tidalcycles/strudel/tree/main/packages/webdirt) +- Speech `.speak()` (experimental) part of [/core](https://github.com/tidalcycles/strudel/tree/main/packages/core) + +These could change, so make sure to check the [packages folder](https://github.com/tidalcycles/strudel/tree/main/packages). From 5fdea7fd8002ae7454bccb88e81e1689e26f55bb Mon Sep 17 00:00:00 2001 From: Alex McLean Date: Thu, 12 Jun 2025 14:36:45 +0100 Subject: [PATCH 106/174] github > strudel --- packages/README.md | 2 +- packages/codemirror/package.json | 6 +++--- packages/core/clockworker.js | 2 +- packages/core/controls.mjs | 12 ++++++------ packages/core/cyclist.mjs | 6 +++--- packages/core/drawLine.mjs | 2 +- packages/core/euclid.mjs | 2 +- packages/core/evaluate.mjs | 2 +- packages/core/fraction.mjs | 2 +- packages/core/hap.mjs | 2 +- packages/core/index.mjs | 2 +- packages/core/neocyclist.mjs | 2 +- packages/core/package.json | 4 ++-- packages/core/pattern.mjs | 2 +- packages/core/pick.mjs | 2 +- packages/core/repl.mjs | 2 +- packages/core/signal.mjs | 2 +- packages/core/speak.mjs | 2 +- packages/core/state.mjs | 2 +- packages/core/timespan.mjs | 2 +- packages/core/ui.mjs | 2 +- packages/core/util.mjs | 2 +- packages/core/value.mjs | 4 ++-- packages/csound/package.json | 6 +++--- packages/desktopbridge/index.mjs | 2 +- packages/desktopbridge/package.json | 6 +++--- packages/draw/draw.mjs | 2 +- packages/draw/package.json | 6 +++--- packages/draw/pianoroll.mjs | 2 +- packages/embed/package.json | 6 +++--- packages/gamepad/package.json | 6 +++--- packages/hs2js/package.json | 6 +++--- packages/hydra/package.json | 6 +++--- packages/midi/midi.mjs | 2 +- packages/midi/package.json | 6 +++--- packages/mini/krill.pegjs | 2 +- packages/mini/mini.mjs | 2 +- packages/mini/package.json | 6 +++--- packages/motion/package.json | 6 +++--- packages/mqtt/mqtt.mjs | 2 +- packages/mqtt/package.json | 6 +++--- packages/osc/osc.mjs | 2 +- packages/osc/package.json | 6 +++--- packages/osc/server.js | 2 +- packages/osc/tidal-sniffer.js | 2 +- packages/reference/package.json | 6 +++--- packages/repl/package.json | 6 +++--- packages/serial/package.json | 6 +++--- packages/serial/serial.mjs | 2 +- packages/soundfonts/package.json | 6 +++--- packages/superdough/index.mjs | 2 +- packages/superdough/package.json | 6 +++--- packages/superdough/superdough.mjs | 2 +- packages/tidal/package.json | 6 +++--- packages/tonal/package.json | 6 +++--- packages/tonal/tonal.mjs | 2 +- packages/tonal/tonleiter.mjs | 2 +- packages/tonal/voicings.mjs | 2 +- packages/transpiler/package.json | 6 +++--- packages/transpiler/transpiler.mjs | 2 +- packages/web/package.json | 6 +++--- packages/webaudio/index.mjs | 2 +- packages/webaudio/package.json | 6 +++--- packages/webaudio/webaudio.mjs | 4 ++-- packages/xen/package.json | 6 +++--- packages/xen/tune.mjs | 2 +- packages/xen/tunejs.js | 2 +- packages/xen/xen.mjs | 2 +- 68 files changed, 126 insertions(+), 126 deletions(-) diff --git a/packages/README.md b/packages/README.md index 7a7681b67..a5b93d321 100644 --- a/packages/README.md +++ b/packages/README.md @@ -2,4 +2,4 @@ Each folder represents one of the @strudel/* packages [published to npm](https://www.npmjs.com/org/strudel). -To understand how those pieces connect, refer to the [Technical Manual](https://github.com/tidalcycles/strudel/wiki/Technical-Manual) or the individual READMEs. +To understand how those pieces connect, refer to the [Technical Manual](https://codeberg.org/uzu/strudel/src/branch/main/technical-manual.md) or the individual READMEs. diff --git a/packages/codemirror/package.json b/packages/codemirror/package.json index 5036cd8a3..1ffe1724c 100644 --- a/packages/codemirror/package.json +++ b/packages/codemirror/package.json @@ -13,7 +13,7 @@ "type": "module", "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git }, "keywords": [ "tidalcycles", @@ -28,9 +28,9 @@ ], "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel#readme", + "homepage": "https://codeberg.org/uzu/strudel#readme", "dependencies": { "@codemirror/autocomplete": "^6.18.4", "@codemirror/commands": "^6.8.0", diff --git a/packages/core/clockworker.js b/packages/core/clockworker.js index bcaf28725..77a45362e 100644 --- a/packages/core/clockworker.js +++ b/packages/core/clockworker.js @@ -113,7 +113,7 @@ self.onconnect = function (e) { port.start(); // Required when using addEventListener. Otherwise called implicitly by onmessage setter. }; -// used to consistently schedule events, for use in a service worker - see +// used to consistently schedule events, for use in a service worker - see function createClock( getTime, callback, // called slightly before each cycle diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 21c183da9..058f4536a 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -1,6 +1,6 @@ /* controls.mjs - Registers audio controls for pattern manipulation and effects. -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ @@ -96,7 +96,7 @@ export const { source, src } = registerControl('source', 'src'); * @example * s("bd sd [~ bd] sd,hh*6").n("<0 1>") */ -// also see https://github.com/tidalcycles/strudel/pull/63 +// also see https://codeberg.org/uzu/strudel/pulls/63 export const { n } = registerControl('n'); /** * Plays the given note name or midi number. A note name consists of @@ -348,7 +348,7 @@ export const { bandf, bpf, bp } = registerControl(['bandf', 'bandq', 'bpenv'], ' * s("bd sd [~ bd] sd").bpf(500).bpq("<0 1 2 3>") * */ -// currently an alias of 'bandq' https://github.com/tidalcycles/strudel/issues/496 +// currently an alias of 'bandq' https://codeberg.org/uzu/strudel/issues/496 // ['bpq'], export const { bandq, bpq } = registerControl('bandq', 'bpq'); /** @@ -855,7 +855,7 @@ export const { fanchor } = registerControl('fanchor'); * s("bd sd [~ bd] sd,hh*8").hpf("<2000 2000:25>") * */ -// currently an alias of 'hcutoff' https://github.com/tidalcycles/strudel/issues/496 +// currently an alias of 'hcutoff' https://codeberg.org/uzu/strudel/issues/496 // ['hpf'], /** * Applies a vibrato to the frequency of the oscillator. @@ -922,7 +922,7 @@ export const { hresonance, hpq } = registerControl('hresonance', 'hpq'); * s("bd sd [~ bd] sd,hh*8").lpf(2000).lpq("<0 10 20 30>") * */ -// currently an alias of 'resonance' https://github.com/tidalcycles/strudel/issues/496 +// currently an alias of 'resonance' https://codeberg.org/uzu/strudel/issues/496 export const { resonance, lpq } = registerControl('resonance', 'lpq'); /** * DJ filter, below 0.5 is low pass filter, above is high pass filter. @@ -1288,7 +1288,7 @@ export const { semitone } = registerControl('semitone'); // TODO: synth param export const { voice } = registerControl('voice'); -// voicings // https://github.com/tidalcycles/strudel/issues/506 +// voicings // https://codeberg.org/uzu/strudel/issues/506 // chord to voice, like C Eb Fm7 G7. the symbols can be defined via addVoicings export const { chord } = registerControl('chord'); // which dictionary to use for the voicings diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index bd2db1223..f28dc604c 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -1,6 +1,6 @@ /* -cyclist.mjs - event scheduler for a single strudel instance. for multi-instance scheduler, see - see -Copyright (C) 2022 Strudel contributors - see +cyclist.mjs - event scheduler for a single strudel instance. for multi-instance scheduler, see - see +Copyright (C) 2022 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ @@ -65,7 +65,7 @@ export class Cyclist { (hap.whole.begin - this.num_cycles_at_cps_change) / this.cps + this.seconds_at_cps_change + latency; const duration = hap.duration / this.cps; // the following line is dumb and only here for backwards compatibility - // see https://github.com/tidalcycles/strudel/pull/1004 + // see https://codeberg.org/uzu/strudel/pulls/1004 const deadline = targetTime - phase; onTrigger?.(hap, deadline, duration, this.cps, targetTime); if (hap.value.cps !== undefined && this.cps != hap.value.cps) { diff --git a/packages/core/drawLine.mjs b/packages/core/drawLine.mjs index 91b86b4ab..7509c0f6f 100644 --- a/packages/core/drawLine.mjs +++ b/packages/core/drawLine.mjs @@ -1,6 +1,6 @@ /* drawLine.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ diff --git a/packages/core/euclid.mjs b/packages/core/euclid.mjs index ad0b01486..1a5be78b4 100644 --- a/packages/core/euclid.mjs +++ b/packages/core/euclid.mjs @@ -2,7 +2,7 @@ euclid.mjs - Bjorklund/Euclidean/Diaspora rhythms Copyright (C) 2023 Rohan Drape and strudel contributors -See for authors of this file. +See for authors of this file. The Bjorklund algorithm implementation is ported from the Haskell Music Theory Haskell module by Rohan Drape - https://rohandrape.net/?t=hmt diff --git a/packages/core/evaluate.mjs b/packages/core/evaluate.mjs index e3e73d596..0559a93a2 100644 --- a/packages/core/evaluate.mjs +++ b/packages/core/evaluate.mjs @@ -1,6 +1,6 @@ /* evaluate.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ diff --git a/packages/core/fraction.mjs b/packages/core/fraction.mjs index dc7fe27a7..2e3bc68ea 100644 --- a/packages/core/fraction.mjs +++ b/packages/core/fraction.mjs @@ -1,6 +1,6 @@ /* fraction.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ diff --git a/packages/core/hap.mjs b/packages/core/hap.mjs index 7a9e0a620..a6e3c55ad 100644 --- a/packages/core/hap.mjs +++ b/packages/core/hap.mjs @@ -1,6 +1,6 @@ /* hap.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ import Fraction from './fraction.mjs'; diff --git a/packages/core/index.mjs b/packages/core/index.mjs index a10b68b09..e4daf445a 100644 --- a/packages/core/index.mjs +++ b/packages/core/index.mjs @@ -1,6 +1,6 @@ /* index.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ diff --git a/packages/core/neocyclist.mjs b/packages/core/neocyclist.mjs index ad22cf006..5c175dc1f 100644 --- a/packages/core/neocyclist.mjs +++ b/packages/core/neocyclist.mjs @@ -1,6 +1,6 @@ /* neocyclist.mjs - event scheduler like cyclist, except recieves clock pulses from clockworker in order to sync across multiple instances. -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ diff --git a/packages/core/package.json b/packages/core/package.json index 89b047d2a..d6853c961 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -15,7 +15,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git }, "keywords": [ "tidalcycles", @@ -27,7 +27,7 @@ "author": "Alex McLean (https://slab.org)", "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, "homepage": "https://strudel.cc", "dependencies": { diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index efbae1f3e..76b02c21e 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -1,6 +1,6 @@ /* pattern.mjs - Core pattern representation for strudel -Copyright (C) 2025 Strudel contributors - see +Copyright (C) 2025 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ diff --git a/packages/core/pick.mjs b/packages/core/pick.mjs index 702201fa6..206fa619b 100644 --- a/packages/core/pick.mjs +++ b/packages/core/pick.mjs @@ -1,6 +1,6 @@ /* pick.mjs - methods that use one pattern to pick events from other patterns. -Copyright (C) 2024 Strudel contributors - see +Copyright (C) 2024 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index e703909ff..7a8cbbab2 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -225,7 +225,7 @@ export function repl({ export const getTrigger = ({ getTime, defaultOutput }) => async (hap, deadline, duration, cps, t) => { - // TODO: get rid of deadline after https://github.com/tidalcycles/strudel/pull/1004 + // TODO: get rid of deadline after https://codeberg.org/uzu/strudel/pulls/1004 try { if (!hap.context.onTrigger || !hap.context.dominantTrigger) { await defaultOutput(hap, deadline, duration, cps, t); diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index 5fd83bce3..54da989a5 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -1,6 +1,6 @@ /* signal.mjs - continuous patterns -Copyright (C) 2024 Strudel contributors - see +Copyright (C) 2024 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ diff --git a/packages/core/speak.mjs b/packages/core/speak.mjs index 6ae959544..7e548a73b 100644 --- a/packages/core/speak.mjs +++ b/packages/core/speak.mjs @@ -1,6 +1,6 @@ /* speak.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ diff --git a/packages/core/state.mjs b/packages/core/state.mjs index db1f77eff..162dc7da9 100644 --- a/packages/core/state.mjs +++ b/packages/core/state.mjs @@ -1,6 +1,6 @@ /* state.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ diff --git a/packages/core/timespan.mjs b/packages/core/timespan.mjs index 4cbfb999a..0dbc74fc8 100644 --- a/packages/core/timespan.mjs +++ b/packages/core/timespan.mjs @@ -1,6 +1,6 @@ /* timespan.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ diff --git a/packages/core/ui.mjs b/packages/core/ui.mjs index 86ceb2863..5a2c54a30 100644 --- a/packages/core/ui.mjs +++ b/packages/core/ui.mjs @@ -1,6 +1,6 @@ /* ui.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ diff --git a/packages/core/util.mjs b/packages/core/util.mjs index b7b1e8413..b81811bae 100644 --- a/packages/core/util.mjs +++ b/packages/core/util.mjs @@ -1,6 +1,6 @@ /* util.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ diff --git a/packages/core/value.mjs b/packages/core/value.mjs index ef98bc370..9496405eb 100644 --- a/packages/core/value.mjs +++ b/packages/core/value.mjs @@ -1,6 +1,6 @@ /* value.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ @@ -9,7 +9,7 @@ import { logger } from './logger.mjs'; export function unionWithObj(a, b, func) { if (b?.value !== undefined && Object.keys(b).length === 1) { - // https://github.com/tidalcycles/strudel/issues/1026 + // https://codeberg.org/uzu/strudel/issues/1026 logger(`[warn]: Can't do arithmetic on control pattern.`); return a; } diff --git a/packages/csound/package.json b/packages/csound/package.json index f200cdaf4..837a397f9 100644 --- a/packages/csound/package.json +++ b/packages/csound/package.json @@ -13,7 +13,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git }, "keywords": [ "tidalcycles", @@ -28,9 +28,9 @@ ], "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel#readme", + "homepage": "https://codeberg.org/uzu/strudel#readme", "dependencies": { "@csound/browser": "6.18.7", "@strudel/core": "workspace:*", diff --git a/packages/desktopbridge/index.mjs b/packages/desktopbridge/index.mjs index 591bbe34f..ffb88783f 100644 --- a/packages/desktopbridge/index.mjs +++ b/packages/desktopbridge/index.mjs @@ -1,6 +1,6 @@ /* index.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ diff --git a/packages/desktopbridge/package.json b/packages/desktopbridge/package.json index 45e89f44e..6609016e0 100644 --- a/packages/desktopbridge/package.json +++ b/packages/desktopbridge/package.json @@ -7,7 +7,7 @@ "type": "module", "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git }, "keywords": [ "tidalcycles", @@ -19,11 +19,11 @@ "author": "Jade Rowland ", "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, "dependencies": { "@strudel/core": "workspace:*", "@tauri-apps/api": "^2.2.0" }, - "homepage": "https://github.com/tidalcycles/strudel#readme" + "homepage": "https://codeberg.org/uzu/strudel#readme" } \ No newline at end of file diff --git a/packages/draw/draw.mjs b/packages/draw/draw.mjs index 0576c297b..c727c1d81 100644 --- a/packages/draw/draw.mjs +++ b/packages/draw/draw.mjs @@ -1,6 +1,6 @@ /* draw.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ diff --git a/packages/draw/package.json b/packages/draw/package.json index 51750da2b..f2555a5c4 100644 --- a/packages/draw/package.json +++ b/packages/draw/package.json @@ -13,7 +13,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git }, "keywords": [ "titdalcycles", @@ -25,9 +25,9 @@ "author": "Felix Roos ", "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel#readme", + "homepage": "https://codeberg.org/uzu/strudel#readme", "dependencies": { "@strudel/core": "workspace:*" }, diff --git a/packages/draw/pianoroll.mjs b/packages/draw/pianoroll.mjs index d874c9686..1cf218fa0 100644 --- a/packages/draw/pianoroll.mjs +++ b/packages/draw/pianoroll.mjs @@ -1,6 +1,6 @@ /* pianoroll.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ diff --git a/packages/embed/package.json b/packages/embed/package.json index afd887887..3b88acc62 100644 --- a/packages/embed/package.json +++ b/packages/embed/package.json @@ -6,7 +6,7 @@ "type": "module", "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git }, "keywords": [ "tidalcycles", @@ -18,7 +18,7 @@ "author": "Felix Roos ", "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel#readme" + "homepage": "https://codeberg.org/uzu/strudel#readme" } diff --git a/packages/gamepad/package.json b/packages/gamepad/package.json index 8b91a15e9..25b4a87b5 100644 --- a/packages/gamepad/package.json +++ b/packages/gamepad/package.json @@ -13,7 +13,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git }, "keywords": [ "titdalcycles", @@ -25,9 +25,9 @@ "author": "Yuta Nakayama ", "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel#readme", + "homepage": "https://codeberg.org/uzu/strudel#readme", "dependencies": { "@strudel/core": "workspace:*" }, diff --git a/packages/hs2js/package.json b/packages/hs2js/package.json index c650dfc83..3b93a3ede 100644 --- a/packages/hs2js/package.json +++ b/packages/hs2js/package.json @@ -16,7 +16,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git }, "keywords": [ "haskell", @@ -25,9 +25,9 @@ "author": "Felix Roos ", "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel", + "homepage": "https://codeberg.org/uzu/strudel/", "dependencies": { "web-tree-sitter": "^0.24.7" }, diff --git a/packages/hydra/package.json b/packages/hydra/package.json index 4375ebefc..c1294eb00 100644 --- a/packages/hydra/package.json +++ b/packages/hydra/package.json @@ -17,7 +17,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git }, "keywords": [ "tidalcycles", @@ -29,9 +29,9 @@ "author": "Felix Roos ", "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel#readme", + "homepage": "https://codeberg.org/uzu/strudel#readme", "dependencies": { "@strudel/core": "workspace:*", "@strudel/draw": "workspace:*", diff --git a/packages/midi/midi.mjs b/packages/midi/midi.mjs index ce7cdb0e2..af2dd3a62 100644 --- a/packages/midi/midi.mjs +++ b/packages/midi/midi.mjs @@ -1,6 +1,6 @@ /* midi.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ diff --git a/packages/midi/package.json b/packages/midi/package.json index 2956b99f3..513dbe197 100644 --- a/packages/midi/package.json +++ b/packages/midi/package.json @@ -13,7 +13,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git }, "keywords": [ "titdalcycles", @@ -25,9 +25,9 @@ "author": "Felix Roos ", "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel#readme", + "homepage": "https://codeberg.org/uzu/strudel#readme", "dependencies": { "@strudel/core": "workspace:*", "@strudel/webaudio": "workspace:*", diff --git a/packages/mini/krill.pegjs b/packages/mini/krill.pegjs index a593b4167..8af82caee 100644 --- a/packages/mini/krill.pegjs +++ b/packages/mini/krill.pegjs @@ -1,6 +1,6 @@ /* krill.pegjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ diff --git a/packages/mini/mini.mjs b/packages/mini/mini.mjs index 6277daa91..d138d9fa0 100644 --- a/packages/mini/mini.mjs +++ b/packages/mini/mini.mjs @@ -1,6 +1,6 @@ /* mini.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ diff --git a/packages/mini/package.json b/packages/mini/package.json index 5ae0dc242..9c96292d7 100644 --- a/packages/mini/package.json +++ b/packages/mini/package.json @@ -16,7 +16,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git }, "keywords": [ "tidalcycles", @@ -28,9 +28,9 @@ "author": "Felix Roos ", "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel#readme", + "homepage": "https://codeberg.org/uzu/strudel#readme", "dependencies": { "@strudel/core": "workspace:*" }, diff --git a/packages/motion/package.json b/packages/motion/package.json index 850a75bd1..1fa87d878 100644 --- a/packages/motion/package.json +++ b/packages/motion/package.json @@ -13,7 +13,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git }, "keywords": [ "titdalcycles", @@ -25,9 +25,9 @@ "author": "Yuta Nakayama ", "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel#readme", + "homepage": "https://codeberg.org/uzu/strudel#readme", "dependencies": { "@strudel/core": "workspace:*" }, diff --git a/packages/mqtt/mqtt.mjs b/packages/mqtt/mqtt.mjs index c2322600c..aef01bd93 100644 --- a/packages/mqtt/mqtt.mjs +++ b/packages/mqtt/mqtt.mjs @@ -1,6 +1,6 @@ /* mqtt.mjs - for patterning the internet of things from strudel -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ diff --git a/packages/mqtt/package.json b/packages/mqtt/package.json index 9b9cdaa3b..0eba694c3 100644 --- a/packages/mqtt/package.json +++ b/packages/mqtt/package.json @@ -13,7 +13,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git }, "keywords": [ "titdalcycles", @@ -25,9 +25,9 @@ "author": "Alex McLean ", "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel#readme", + "homepage": "https://codeberg.org/uzu/strudel#readme", "dependencies": { "@strudel/core": "workspace:*", "paho-mqtt": "^1.1.0" diff --git a/packages/osc/osc.mjs b/packages/osc/osc.mjs index 3c7b92d4e..ac70b9e73 100644 --- a/packages/osc/osc.mjs +++ b/packages/osc/osc.mjs @@ -1,6 +1,6 @@ /* osc.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ diff --git a/packages/osc/package.json b/packages/osc/package.json index 7e87c1e1a..62b00090a 100644 --- a/packages/osc/package.json +++ b/packages/osc/package.json @@ -17,7 +17,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git }, "keywords": [ "tidalcycles", @@ -32,9 +32,9 @@ ], "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel#readme", + "homepage": "https://codeberg.org/uzu/strudel#readme", "dependencies": { "@strudel/core": "workspace:*", "osc-js": "^2.4.1" diff --git a/packages/osc/server.js b/packages/osc/server.js index 7e0b90592..75fc5b1c0 100644 --- a/packages/osc/server.js +++ b/packages/osc/server.js @@ -1,6 +1,6 @@ /* server.js - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ diff --git a/packages/osc/tidal-sniffer.js b/packages/osc/tidal-sniffer.js index 10bf0ad05..95e1fc329 100644 --- a/packages/osc/tidal-sniffer.js +++ b/packages/osc/tidal-sniffer.js @@ -1,6 +1,6 @@ /* tidal-sniffer.js - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ diff --git a/packages/reference/package.json b/packages/reference/package.json index fd7edadff..289520ddd 100644 --- a/packages/reference/package.json +++ b/packages/reference/package.json @@ -13,7 +13,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git }, "keywords": [ "tidalcycles", @@ -28,9 +28,9 @@ ], "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel#readme", + "homepage": "https://codeberg.org/uzu/strudel#readme", "devDependencies": { "vite": "^6.0.11" } diff --git a/packages/repl/package.json b/packages/repl/package.json index 5487ce443..1c10aebf7 100644 --- a/packages/repl/package.json +++ b/packages/repl/package.json @@ -14,7 +14,7 @@ "type": "module", "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git }, "keywords": [ "tidalcycles", @@ -29,9 +29,9 @@ ], "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel#readme", + "homepage": "https://codeberg.org/uzu/strudel#readme", "dependencies": { "@strudel/codemirror": "workspace:*", "@strudel/core": "workspace:*", diff --git a/packages/serial/package.json b/packages/serial/package.json index bf6e0dcab..df671cb1a 100644 --- a/packages/serial/package.json +++ b/packages/serial/package.json @@ -13,7 +13,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git }, "keywords": [ "titdalcycles", @@ -25,9 +25,9 @@ "author": "Alex McLean ", "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel#readme", + "homepage": "https://codeberg.org/uzu/strudel#readme", "dependencies": { "@strudel/core": "workspace:*" }, diff --git a/packages/serial/serial.mjs b/packages/serial/serial.mjs index e0eeacedd..692109522 100644 --- a/packages/serial/serial.mjs +++ b/packages/serial/serial.mjs @@ -1,6 +1,6 @@ /* serial.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ diff --git a/packages/soundfonts/package.json b/packages/soundfonts/package.json index bf4fa2960..d227359c7 100644 --- a/packages/soundfonts/package.json +++ b/packages/soundfonts/package.json @@ -13,7 +13,7 @@ "type": "module", "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git }, "keywords": [ "tidalcycles", @@ -25,9 +25,9 @@ "author": "Felix Roos ", "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel#readme", + "homepage": "https://codeberg.org/uzu/strudel#readme", "dependencies": { "@strudel/core": "workspace:*", "@strudel/webaudio": "workspace:*", diff --git a/packages/superdough/index.mjs b/packages/superdough/index.mjs index 3247c5b49..fd49fe338 100644 --- a/packages/superdough/index.mjs +++ b/packages/superdough/index.mjs @@ -1,6 +1,6 @@ /* index.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ diff --git a/packages/superdough/package.json b/packages/superdough/package.json index a835f252d..8fc3cca78 100644 --- a/packages/superdough/package.json +++ b/packages/superdough/package.json @@ -16,7 +16,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git }, "keywords": [ "tidalcycles", @@ -28,9 +28,9 @@ "author": "Felix Roos ", "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel#readme", + "homepage": "https://codeberg.org/uzu/strudel#readme", "devDependencies": { "vite": "^6.0.11", "vite-plugin-bundle-audioworklet": "workspace:*" diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 1069d4e84..819cdeb37 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -1,6 +1,6 @@ /* superdough.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ diff --git a/packages/tidal/package.json b/packages/tidal/package.json index 8e4871617..960b09001 100644 --- a/packages/tidal/package.json +++ b/packages/tidal/package.json @@ -6,7 +6,7 @@ "module": "tidal.mjs", "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel/tree/main/packages/tidal" + "url": "git+https://codeberg.org/uzu/strudel/src/branch/main/packages/tidal" }, "keywords": [ "haskell", @@ -15,9 +15,9 @@ "author": "Felix Roos ", "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel/tree/main/packages/hs2js", + "homepage": "https://codeberg.org/uzu/strudel/src/branch/main/packages/hs2js", "dependencies": { "@strudel/core": "workspace:*", "@strudel/mini": "workspace:*", diff --git a/packages/tonal/package.json b/packages/tonal/package.json index 89f02b301..98b2a5325 100644 --- a/packages/tonal/package.json +++ b/packages/tonal/package.json @@ -14,7 +14,7 @@ "type": "module", "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git }, "keywords": [ "tidalcycles", @@ -26,9 +26,9 @@ "author": "Felix Roos ", "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel#readme", + "homepage": "https://codeberg.org/uzu/strudel#readme", "dependencies": { "@strudel/core": "workspace:*", "@tonaljs/tonal": "^4.10.0", diff --git a/packages/tonal/tonal.mjs b/packages/tonal/tonal.mjs index 78183d228..4fd622158 100644 --- a/packages/tonal/tonal.mjs +++ b/packages/tonal/tonal.mjs @@ -1,6 +1,6 @@ /* tonal.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ diff --git a/packages/tonal/tonleiter.mjs b/packages/tonal/tonleiter.mjs index 15288fcc8..3814394f6 100644 --- a/packages/tonal/tonleiter.mjs +++ b/packages/tonal/tonleiter.mjs @@ -178,7 +178,7 @@ export function renderVoicing({ chord, dictionary, offset = 0, n, mode = 'below' return notes; } -// https://github.com/tidalcycles/strudel/blob/14184993d0ee7d69c47df57ac864a1a0f99a893f/packages/tonal/tonleiter.mjs +// https://codeberg.org/uzu/strudel/blob/14184993d0ee7d69c47df57ac864a1a0f99a893f/packages/tonal/tonleiter.mjs const steps = [1, 0, 2, 0, 3, 4, 0, 5, 0, 6, 0, 7]; const notes = ['C', '', 'D', '', 'E', 'F', '', 'G', '', 'A', '', 'B']; const noteLetters = ['C', 'D', 'E', 'F', 'G', 'A', 'B']; diff --git a/packages/tonal/voicings.mjs b/packages/tonal/voicings.mjs index 0a08575db..d81911e01 100644 --- a/packages/tonal/voicings.mjs +++ b/packages/tonal/voicings.mjs @@ -1,6 +1,6 @@ /* voicings.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ diff --git a/packages/transpiler/package.json b/packages/transpiler/package.json index 93648bbc7..1a20a78f8 100644 --- a/packages/transpiler/package.json +++ b/packages/transpiler/package.json @@ -14,7 +14,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git }, "keywords": [ "tidalcycles", @@ -26,9 +26,9 @@ "author": "Felix Roos ", "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel#readme", + "homepage": "https://codeberg.org/uzu/strudel#readme", "dependencies": { "@strudel/core": "workspace:*", "@strudel/mini": "workspace:*", diff --git a/packages/transpiler/transpiler.mjs b/packages/transpiler/transpiler.mjs index 2e566305f..2f5ed5309 100644 --- a/packages/transpiler/transpiler.mjs +++ b/packages/transpiler/transpiler.mjs @@ -194,7 +194,7 @@ function isLabelStatement(node) { } // converts label expressions to p calls: "x: y" to "y.p('x')" -// see https://github.com/tidalcycles/strudel/issues/990 +// see https://codeberg.org/uzu/strudel/issues/990 function labelToP(node) { return { type: 'ExpressionStatement', diff --git a/packages/web/package.json b/packages/web/package.json index 652496fee..6264849eb 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -14,7 +14,7 @@ "type": "module", "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git }, "keywords": [ "tidalcycles", @@ -29,9 +29,9 @@ ], "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel#readme", + "homepage": "https://codeberg.org/uzu/strudel#readme", "dependencies": { "@strudel/core": "workspace:*", "@strudel/mini": "workspace:*", diff --git a/packages/webaudio/index.mjs b/packages/webaudio/index.mjs index 59672b617..362e61c44 100644 --- a/packages/webaudio/index.mjs +++ b/packages/webaudio/index.mjs @@ -1,6 +1,6 @@ /* index.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ diff --git a/packages/webaudio/package.json b/packages/webaudio/package.json index 5714fddf3..f984d5321 100644 --- a/packages/webaudio/package.json +++ b/packages/webaudio/package.json @@ -17,7 +17,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git }, "keywords": [ "tidalcycles", @@ -29,9 +29,9 @@ "author": "Felix Roos ", "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel#readme", + "homepage": "https://codeberg.org/uzu/strudel#readme", "dependencies": { "@strudel/core": "workspace:*", "@strudel/draw": "workspace:*", diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 44a683480..f80f114fd 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -1,6 +1,6 @@ /* webaudio.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ @@ -16,7 +16,7 @@ const hap2value = (hap) => { }; export const webaudioOutputTrigger = (t, hap, ct, cps) => superdough(hap2value(hap), t - ct, hap.duration / cps, cps); -// uses more precise, absolute t if available, see https://github.com/tidalcycles/strudel/pull/1004 +// uses more precise, absolute t if available, see https://codeberg.org/uzu/strudel/pulls/1004 export const webaudioOutput = (hap, deadline, hapDuration, cps, t) => superdough(hap2value(hap), t ? `=${t}` : deadline, hapDuration); diff --git a/packages/xen/package.json b/packages/xen/package.json index 76fecf525..b79aea6e6 100644 --- a/packages/xen/package.json +++ b/packages/xen/package.json @@ -14,7 +14,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git }, "keywords": [ "tidalcycles", @@ -26,9 +26,9 @@ "author": "Felix Roos ", "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel#readme", + "homepage": "https://codeberg.org/uzu/strudel#readme", "dependencies": { "@strudel/core": "workspace:*" }, diff --git a/packages/xen/tune.mjs b/packages/xen/tune.mjs index feed38f4f..01303bf52 100644 --- a/packages/xen/tune.mjs +++ b/packages/xen/tune.mjs @@ -1,6 +1,6 @@ /* tune.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ diff --git a/packages/xen/tunejs.js b/packages/xen/tunejs.js index 7b1a804a0..6b5e7cb7c 100644 --- a/packages/xen/tunejs.js +++ b/packages/xen/tunejs.js @@ -1,6 +1,6 @@ /* tunejs.js - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ diff --git a/packages/xen/xen.mjs b/packages/xen/xen.mjs index 4077632a5..cc96f4110 100644 --- a/packages/xen/xen.mjs +++ b/packages/xen/xen.mjs @@ -1,6 +1,6 @@ /* xen.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ From 307e5ea2d7536dcef26b2e7d453b797bd0f78e87 Mon Sep 17 00:00:00 2001 From: Alex McLean Date: Thu, 12 Jun 2025 14:45:35 +0100 Subject: [PATCH 107/174] less github --- package.json | 4 ++-- technical.manual.md | 24 ++++++++++++------------ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 01c3e7d5d..be3e1f1c8 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git }, "keywords": [ "tidalcycles", @@ -43,7 +43,7 @@ "author": "Alex McLean (https://slab.org)", "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, "homepage": "https://strudel.cc", "dependencies": { diff --git a/technical.manual.md b/technical.manual.md index 73180b145..9b09c741d 100644 --- a/technical.manual.md +++ b/technical.manual.md @@ -7,13 +7,13 @@ There are different packages for different purposes. They.. - split up the code into smaller chunks - can be selectively used to implement some sort of time based system -Please refer to the individual README files in the [packages folder](https://github.com/tidalcycles/strudel/tree/main/packages) +Please refer to the individual README files in the [packages folder](https://codeberg.org/uzu/strudel/src/branch/main/packages) ## REPL The [REPL](https://strudel.tidalcycles.org/) is the place where all packages come together to form a live coding system. It can also be seen as a reference implementation for users of the library. -More info in the [REPL README](https://github.com/tidalcycles/strudel/tree/main/repl) +More info in the [REPL README](https://codeberg.org/uzu/strudel/src/branch/main/packages/repl/README.md) # High Level Overview @@ -21,7 +21,7 @@ More info in the [REPL README](https://github.com/tidalcycles/strudel/tree/main/ ## 1. End User Code -The End User Code is written in JavaScript with added syntax sugar. The [eval package](https://github.com/tidalcycles/strudel/tree/main/packages/eval#strudelcycleseval) evaluates the user code +The End User Code is written in JavaScript with added syntax sugar. The [eval package](https://codeberg.org/uzu/strudel/src/branch/main/packages/eval#strudelcycleseval) evaluates the user code after a transpilation step, which resolves the syntax sugar. If you don't want the syntax sugar, you can omit the eval package and call the native javascript `eval` instead. ### 🍭 Syntax Sugar @@ -60,7 +60,7 @@ Shift will most likely be replaced with acorn in the future, see https://github. Another important part of the user code is the mini notation, which allows to express rhythms in a short manner. -- the mini notation is [implemented as a PEG grammar](https://github.com/tidalcycles/strudel/blob/main/packages/mini/krill.pegjs), living in the [mini package](https://github.com/tidalcycles/strudel/tree/main/packages/mini) +- the mini notation is [implemented as a PEG grammar](https://codeberg.org/uzu/strudel/src/branch/main/packages/mini/krill.pegjs), living in the [mini package](https://codeberg.org/uzu/strudel/src/branch/main/packages/mini) - it is based on [krill](https://github.com/Mdashdotdashn/krill) by Mdashdotdashn - the peg grammar is used to generate a parser with [peggyjs](https://peggyjs.org/) - the generated parser takes a mini notation string and outputs an AST @@ -182,12 +182,12 @@ Here is an example Hap value with different properties: At the time of writing this doc, the following outputs are supported: -- Web Audio API `.out()` see [/webaudio](https://github.com/tidalcycles/strudel/tree/main/packages/webaudio) -- MIDI `.midi()` see [/midi](https://github.com/tidalcycles/strudel/tree/main/packages/midi) -- OSC `.osc()` see [/osc](https://github.com/tidalcycles/strudel/tree/main/packages/osc) -- Serial `.serial()` see [/serial](https://github.com/tidalcycles/strudel/tree/main/packages/serial) -- Tone.js `.tone()` (deprecated?) [/tone](https://github.com/tidalcycles/strudel/tree/main/packages/tone) -- WebDirt `.webdirt()` (deprecated?) [/webdirt](https://github.com/tidalcycles/strudel/tree/main/packages/webdirt) -- Speech `.speak()` (experimental) part of [/core](https://github.com/tidalcycles/strudel/tree/main/packages/core) +- Web Audio API `.out()` see [/webaudio](https://codeberg.org/uzu/strudel/src/branch/main/packages/webaudio) +- MIDI `.midi()` see [/midi](https://codeberg.org/uzu/strudel/src/branch/main/packages/midi) +- OSC `.osc()` see [/osc](https://codeberg.org/uzu/strudel/src/branch/main/packages/osc) +- Serial `.serial()` see [/serial](https://codeberg.org/uzu/strudel/src/branch/main/packages/serial) +- Tone.js `.tone()` (deprecated?) [/tone](https://codeberg.org/uzu/strudel/src/branch/main/packages/tone) +- WebDirt `.webdirt()` (deprecated?) [/webdirt](https://codeberg.org/uzu/strudel/src/branch/main/packages/webdirt) +- Speech `.speak()` (experimental) part of [/core](https://codeberg.org/uzu/strudel/src/branch/main/packages/core) -These could change, so make sure to check the [packages folder](https://github.com/tidalcycles/strudel/tree/main/packages). +These could change, so make sure to check the [packages folder](https://codeberg.org/uzu/strudel/src/branch/main/packages). From c0ae12f32f49b3386bd3e07d8ffc754545df84bd Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 12 Jun 2025 17:26:19 +0100 Subject: [PATCH 108/174] add codeberg repo update command --- CONTRIBUTING.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 618830b9c..aa84cbce7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,6 +6,13 @@ Thanks for wanting to contribute!!! There are many ways you can add value to thi We are currently in the process of moving from github to codeberg -- not everything is working, please bear with us. +To update your local clone, you can run this command: + +``` +git remote set-url origin git@codeberg.org:uzu/strudel.git +``` + + ## Communication Channels To get in touch with the contributors, either From 990f1c6ece10f61dca48d23b81d95c755bf99d54 Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 12 Jun 2025 17:29:29 +0100 Subject: [PATCH 109/174] degithub --- README.md | 2 +- technical.manual.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 12ee85035..e225d9bde 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ Licensing info for the default sound banks can be found over on the [dough-sampl There are many ways to contribute to this project! See [contribution guide](./CONTRIBUTING.md). - + diff --git a/technical.manual.md b/technical.manual.md index 9b09c741d..58f7cd934 100644 --- a/technical.manual.md +++ b/technical.manual.md @@ -54,7 +54,7 @@ This is how it works: - The AST is transformed to resolve the syntax sugar - The AST is used to generate code again (shift-codegen) -Shift will most likely be replaced with acorn in the future, see https://github.com/tidalcycles/strudel/issues/174 +Shift will most likely be replaced with acorn in the future, see https://codeberg.org/uzu/strudel/issues/174 ### Mini Notation From a3d9d68c45fb0ccbf883acfff68b15bcbb235b7b Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 12 Jun 2025 18:00:39 +0100 Subject: [PATCH 110/174] less github --- examples/tidal-repl/package.json | 6 +++--- jsdoc/jsdoc-synonyms.js | 2 +- my-patterns/README.md | 22 +++++++++------------- packages/core/test/value.test.mjs | 2 +- packages/hs2js/README.md | 2 +- website/agpl-header.txt | 4 ++-- website/src/config.ts | 2 +- website/src/user_pattern_utils.mjs | 2 +- 8 files changed, 19 insertions(+), 23 deletions(-) diff --git a/examples/tidal-repl/package.json b/examples/tidal-repl/package.json index 4da2f086b..7c1f55c38 100644 --- a/examples/tidal-repl/package.json +++ b/examples/tidal-repl/package.json @@ -11,7 +11,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git }, "keywords": [ "titdalcycles", @@ -23,9 +23,9 @@ "author": "Felix Roos ", "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel#readme", + "homepage": "https://codeberg.org/uzu/strudel#readme", "dependencies": { "@strudel/web": "workspace:*", "hs2js": "workspace:*" diff --git a/jsdoc/jsdoc-synonyms.js b/jsdoc/jsdoc-synonyms.js index 09190846f..0b52420bc 100644 --- a/jsdoc/jsdoc-synonyms.js +++ b/jsdoc/jsdoc-synonyms.js @@ -1,6 +1,6 @@ /* jsdoc-synonyms.js - Add support for @synonym tag -Copyright (C) 2023 Strudel contributors - see +Copyright (C) 2023 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ diff --git a/my-patterns/README.md b/my-patterns/README.md index c8d694ea8..5283ad81b 100644 --- a/my-patterns/README.md +++ b/my-patterns/README.md @@ -5,22 +5,24 @@ made into a pattern swatch. Example: +Please note: These instructions have not been fully tested/adapted since strudel moved to codeberg from github. PRs welcome! + ## deploy -### 1. fork the [strudel repo on github](https://github.com/tidalcycles/strudel.git) +### 1. fork the [strudel repo on codeberg](https://codeberg.org/uzu/strudel.git) -### 2. clone your fork to your machine `git clone https://github.com//strudel.git strudel && cd strudel` +### 2. clone your fork to your machine `git clone https://codeberg.org//strudel.git strudel && cd strudel` ### 3. create a separate branch like `git branch patternuary && git checkout patternuary` ### 4. save one or more .txt files in the my-patterns folder -### 5. edit `website/public/CNAME` to contain `.github.io/strudel` +### 5. edit `website/public/CNAME` to contain `.codeberg.page/strudel` -### 6. edit `website/astro.config.mjs` to use site: `https://.github.io` and base `/strudel`, like this +### 6. edit `website/astro.config.mjs` to use site: `https://.codeberg.page` and base `/strudel`, like this ```js -const site = 'https://.github.io'; +const site = 'https://.codeberg.page'; const base = '/strudel'; ``` @@ -30,15 +32,9 @@ const base = '/strudel'; git add . && git commit -m "site config" && git push --set-upstream origin ``` -### 8. deploy to github pages +### 8. deploy to codeberg pages -- go to settings -> pages and select "Github Actions" as source -- go to settings -> environments -> github-pages and press the edit button next to `main` and type in `patternuary` (under "Deployment branches") -- go to Actions -> `Build and Deploy` and click `Run workflow` with branch `patternuary` - -### 9. view your patterns at `.github.io/strudel/swatch/` - -Alternatively, github pages allows you to use a custom domain, like https://mycooldomain.org/swatch/. [See their documentation for details](https://docs.github.com/en/pages/configuring-a-custom-domain-for-your-github-pages-site). +### 9. view your patterns at `.codeberg.page/strudel/swatch/` ### 10. optional: automatic deployment diff --git a/packages/core/test/value.test.mjs b/packages/core/test/value.test.mjs index 87cba57d0..35d9f5e10 100644 --- a/packages/core/test/value.test.mjs +++ b/packages/core/test/value.test.mjs @@ -1,6 +1,6 @@ /* value.test.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2025 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ diff --git a/packages/hs2js/README.md b/packages/hs2js/README.md index 24d9c7749..f84f9cded 100644 --- a/packages/hs2js/README.md +++ b/packages/hs2js/README.md @@ -2,7 +2,7 @@ Experimental haskell in javascript interpreter. Many haskell features are not implemented. This projects mainly exists to be able to write and interpret [Tidal Cycles](https://tidalcycles.org/) code in the browser, -as part of [Strudel](https://github.com/tidalcycles/strudel). This project could only exist thanks to [tree-sitter-haskell](https://github.com/tree-sitter/tree-sitter-haskell). +as part of [Strudel](https://codeberg.org/uzu/strudel). This project could only exist thanks to [tree-sitter-haskell](https://github.com/tree-sitter/tree-sitter-haskell). ## Installation diff --git a/website/agpl-header.txt b/website/agpl-header.txt index 6fd0c0fc9..8b7b6d631 100644 --- a/website/agpl-header.txt +++ b/website/agpl-header.txt @@ -1,10 +1,10 @@ /* Strudel - javascript-based environment for live coding algorithmic (musical) patterns -https://strudel.cc / https://github.com/tidalcycles/strudel/ +https://strudel.cc / https://codeberg.org/uzu/strudel/ Copyright (C) Strudel contributors -https://github.com/tidalcycles/strudel/graphs/contributors +https://codeberg.org/uzu/strudel/activity/contributors This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by diff --git a/website/src/config.ts b/website/src/config.ts index e88b7f3e1..490a067b2 100644 --- a/website/src/config.ts +++ b/website/src/config.ts @@ -28,7 +28,7 @@ export const KNOWN_LANGUAGES = { } as const; export const KNOWN_LANGUAGE_CODES = Object.values(KNOWN_LANGUAGES); -export const GITHUB_EDIT_URL = `https://github.com/tidalcycles/strudel/tree/main/website`; +export const GITHUB_EDIT_URL = `https://codeberg.org/uzu/strudel/src/branch/main/website`; export const COMMUNITY_INVITE_URL = `https://discord.com/invite/HGEdXmRkzT`; diff --git a/website/src/user_pattern_utils.mjs b/website/src/user_pattern_utils.mjs index 18442cb4c..791c6a8f9 100644 --- a/website/src/user_pattern_utils.mjs +++ b/website/src/user_pattern_utils.mjs @@ -96,7 +96,7 @@ export async function loadDBPatterns() { } } -// reason: https://github.com/tidalcycles/strudel/issues/857 +// reason: https://codeberg.org/uzu/strudel/issues/857 const $activePattern = sessionAtom('activePattern', ''); export function setActivePattern(key) { From fec38bd5d2d0f0a0162bbc08325cecf6fcc84d31 Mon Sep 17 00:00:00 2001 From: yaxu Date: Fri, 13 Jun 2025 00:07:24 +0200 Subject: [PATCH 111/174] Update .github/workflows/test.yml --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 180f87521..6f2246790 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,7 +4,7 @@ on: [push, pull_request] jobs: build: - runs-on: ubuntu-latest + runs-on: toplap-runner strategy: matrix: node-version: [20] From 1141b1803b92469ef87a7be3022c7cf7a321599d Mon Sep 17 00:00:00 2001 From: yaxu Date: Fri, 13 Jun 2025 00:08:39 +0200 Subject: [PATCH 112/174] Update .github/workflows/test.yml --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6f2246790..180f87521 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,7 +4,7 @@ on: [push, pull_request] jobs: build: - runs-on: toplap-runner + runs-on: ubuntu-latest strategy: matrix: node-version: [20] From 8c8e91417079fd90629d7067d21e19697d53fe3c Mon Sep 17 00:00:00 2001 From: yaxu Date: Fri, 13 Jun 2025 08:53:53 +0200 Subject: [PATCH 113/174] Update .github/workflows/test.yml --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 180f87521..498cd800b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,7 +4,7 @@ on: [push, pull_request] jobs: build: - runs-on: ubuntu-latest + runs-on: docker strategy: matrix: node-version: [20] From 062201d1dce00779f400236abd38724843571f1d Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 13 Jun 2025 08:07:35 +0100 Subject: [PATCH 114/174] fix json --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index be3e1f1c8..d18291eb2 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ }, "repository": { "type": "git", - "url": "git+https://codeberg.org/uzu/strudel.git + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "tidalcycles", @@ -74,4 +74,4 @@ "vitest": "^3.0.4", "vite-plugin-bundle-audioworklet": "workspace:*" } -} +} \ No newline at end of file From dcac254790eafe8a35155da2c77595ea2ddc0b09 Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 13 Jun 2025 08:17:55 +0100 Subject: [PATCH 115/174] missing double quotes --- examples/tidal-repl/package.json | 2 +- packages/codemirror/package.json | 2 +- packages/core/package.json | 2 +- packages/csound/package.json | 2 +- packages/desktopbridge/package.json | 4 ++-- packages/draw/package.json | 2 +- packages/embed/package.json | 2 +- packages/gamepad/package.json | 2 +- packages/hs2js/package.json | 2 +- packages/hydra/package.json | 2 +- packages/midi/package.json | 2 +- packages/mini/package.json | 2 +- packages/motion/package.json | 2 +- packages/mqtt/package.json | 2 +- packages/osc/package.json | 2 +- packages/reference/package.json | 2 +- packages/repl/package.json | 2 +- packages/serial/package.json | 2 +- packages/soundfonts/package.json | 2 +- packages/superdough/package.json | 2 +- packages/tonal/package.json | 2 +- packages/transpiler/package.json | 2 +- packages/web/package.json | 2 +- packages/webaudio/package.json | 2 +- packages/xen/package.json | 2 +- 25 files changed, 26 insertions(+), 26 deletions(-) diff --git a/examples/tidal-repl/package.json b/examples/tidal-repl/package.json index 7c1f55c38..21c8ee177 100644 --- a/examples/tidal-repl/package.json +++ b/examples/tidal-repl/package.json @@ -11,7 +11,7 @@ }, "repository": { "type": "git", - "url": "git+https://codeberg.org/uzu/strudel.git + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "titdalcycles", diff --git a/packages/codemirror/package.json b/packages/codemirror/package.json index 1ffe1724c..4f8508c90 100644 --- a/packages/codemirror/package.json +++ b/packages/codemirror/package.json @@ -13,7 +13,7 @@ "type": "module", "repository": { "type": "git", - "url": "git+https://codeberg.org/uzu/strudel.git + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "tidalcycles", diff --git a/packages/core/package.json b/packages/core/package.json index d6853c961..f4170f2b5 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -15,7 +15,7 @@ }, "repository": { "type": "git", - "url": "git+https://codeberg.org/uzu/strudel.git + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "tidalcycles", diff --git a/packages/csound/package.json b/packages/csound/package.json index 837a397f9..04a5ff246 100644 --- a/packages/csound/package.json +++ b/packages/csound/package.json @@ -13,7 +13,7 @@ }, "repository": { "type": "git", - "url": "git+https://codeberg.org/uzu/strudel.git + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "tidalcycles", diff --git a/packages/desktopbridge/package.json b/packages/desktopbridge/package.json index 6609016e0..a01ec1f8a 100644 --- a/packages/desktopbridge/package.json +++ b/packages/desktopbridge/package.json @@ -7,7 +7,7 @@ "type": "module", "repository": { "type": "git", - "url": "git+https://codeberg.org/uzu/strudel.git + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "tidalcycles", @@ -26,4 +26,4 @@ "@tauri-apps/api": "^2.2.0" }, "homepage": "https://codeberg.org/uzu/strudel#readme" -} \ No newline at end of file +} diff --git a/packages/draw/package.json b/packages/draw/package.json index f2555a5c4..ee1b8dd00 100644 --- a/packages/draw/package.json +++ b/packages/draw/package.json @@ -13,7 +13,7 @@ }, "repository": { "type": "git", - "url": "git+https://codeberg.org/uzu/strudel.git + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "titdalcycles", diff --git a/packages/embed/package.json b/packages/embed/package.json index 3b88acc62..a0cc33de1 100644 --- a/packages/embed/package.json +++ b/packages/embed/package.json @@ -6,7 +6,7 @@ "type": "module", "repository": { "type": "git", - "url": "git+https://codeberg.org/uzu/strudel.git + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "tidalcycles", diff --git a/packages/gamepad/package.json b/packages/gamepad/package.json index 25b4a87b5..3efb2e084 100644 --- a/packages/gamepad/package.json +++ b/packages/gamepad/package.json @@ -13,7 +13,7 @@ }, "repository": { "type": "git", - "url": "git+https://codeberg.org/uzu/strudel.git + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "titdalcycles", diff --git a/packages/hs2js/package.json b/packages/hs2js/package.json index 3b93a3ede..c0bf8fa8c 100644 --- a/packages/hs2js/package.json +++ b/packages/hs2js/package.json @@ -16,7 +16,7 @@ }, "repository": { "type": "git", - "url": "git+https://codeberg.org/uzu/strudel.git + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "haskell", diff --git a/packages/hydra/package.json b/packages/hydra/package.json index c1294eb00..b022de87d 100644 --- a/packages/hydra/package.json +++ b/packages/hydra/package.json @@ -17,7 +17,7 @@ }, "repository": { "type": "git", - "url": "git+https://codeberg.org/uzu/strudel.git + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "tidalcycles", diff --git a/packages/midi/package.json b/packages/midi/package.json index 513dbe197..4efd329d8 100644 --- a/packages/midi/package.json +++ b/packages/midi/package.json @@ -13,7 +13,7 @@ }, "repository": { "type": "git", - "url": "git+https://codeberg.org/uzu/strudel.git + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "titdalcycles", diff --git a/packages/mini/package.json b/packages/mini/package.json index 9c96292d7..5d94301d4 100644 --- a/packages/mini/package.json +++ b/packages/mini/package.json @@ -16,7 +16,7 @@ }, "repository": { "type": "git", - "url": "git+https://codeberg.org/uzu/strudel.git + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "tidalcycles", diff --git a/packages/motion/package.json b/packages/motion/package.json index 1fa87d878..a7db05680 100644 --- a/packages/motion/package.json +++ b/packages/motion/package.json @@ -13,7 +13,7 @@ }, "repository": { "type": "git", - "url": "git+https://codeberg.org/uzu/strudel.git + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "titdalcycles", diff --git a/packages/mqtt/package.json b/packages/mqtt/package.json index 0eba694c3..f522e3354 100644 --- a/packages/mqtt/package.json +++ b/packages/mqtt/package.json @@ -13,7 +13,7 @@ }, "repository": { "type": "git", - "url": "git+https://codeberg.org/uzu/strudel.git + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "titdalcycles", diff --git a/packages/osc/package.json b/packages/osc/package.json index 62b00090a..7d19fbbfc 100644 --- a/packages/osc/package.json +++ b/packages/osc/package.json @@ -17,7 +17,7 @@ }, "repository": { "type": "git", - "url": "git+https://codeberg.org/uzu/strudel.git + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "tidalcycles", diff --git a/packages/reference/package.json b/packages/reference/package.json index 289520ddd..8dc966cc2 100644 --- a/packages/reference/package.json +++ b/packages/reference/package.json @@ -13,7 +13,7 @@ }, "repository": { "type": "git", - "url": "git+https://codeberg.org/uzu/strudel.git + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "tidalcycles", diff --git a/packages/repl/package.json b/packages/repl/package.json index 1c10aebf7..bfa404c75 100644 --- a/packages/repl/package.json +++ b/packages/repl/package.json @@ -14,7 +14,7 @@ "type": "module", "repository": { "type": "git", - "url": "git+https://codeberg.org/uzu/strudel.git + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "tidalcycles", diff --git a/packages/serial/package.json b/packages/serial/package.json index df671cb1a..c04a69cd0 100644 --- a/packages/serial/package.json +++ b/packages/serial/package.json @@ -13,7 +13,7 @@ }, "repository": { "type": "git", - "url": "git+https://codeberg.org/uzu/strudel.git + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "titdalcycles", diff --git a/packages/soundfonts/package.json b/packages/soundfonts/package.json index d227359c7..2c87a6e05 100644 --- a/packages/soundfonts/package.json +++ b/packages/soundfonts/package.json @@ -13,7 +13,7 @@ "type": "module", "repository": { "type": "git", - "url": "git+https://codeberg.org/uzu/strudel.git + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "tidalcycles", diff --git a/packages/superdough/package.json b/packages/superdough/package.json index 8fc3cca78..439b83718 100644 --- a/packages/superdough/package.json +++ b/packages/superdough/package.json @@ -16,7 +16,7 @@ }, "repository": { "type": "git", - "url": "git+https://codeberg.org/uzu/strudel.git + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "tidalcycles", diff --git a/packages/tonal/package.json b/packages/tonal/package.json index 98b2a5325..614e86f74 100644 --- a/packages/tonal/package.json +++ b/packages/tonal/package.json @@ -14,7 +14,7 @@ "type": "module", "repository": { "type": "git", - "url": "git+https://codeberg.org/uzu/strudel.git + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "tidalcycles", diff --git a/packages/transpiler/package.json b/packages/transpiler/package.json index 1a20a78f8..2a5e39776 100644 --- a/packages/transpiler/package.json +++ b/packages/transpiler/package.json @@ -14,7 +14,7 @@ }, "repository": { "type": "git", - "url": "git+https://codeberg.org/uzu/strudel.git + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "tidalcycles", diff --git a/packages/web/package.json b/packages/web/package.json index 6264849eb..0feddc82d 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -14,7 +14,7 @@ "type": "module", "repository": { "type": "git", - "url": "git+https://codeberg.org/uzu/strudel.git + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "tidalcycles", diff --git a/packages/webaudio/package.json b/packages/webaudio/package.json index f984d5321..5cc0a5538 100644 --- a/packages/webaudio/package.json +++ b/packages/webaudio/package.json @@ -17,7 +17,7 @@ }, "repository": { "type": "git", - "url": "git+https://codeberg.org/uzu/strudel.git + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "tidalcycles", diff --git a/packages/xen/package.json b/packages/xen/package.json index b79aea6e6..88c2bb082 100644 --- a/packages/xen/package.json +++ b/packages/xen/package.json @@ -14,7 +14,7 @@ }, "repository": { "type": "git", - "url": "git+https://codeberg.org/uzu/strudel.git + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "tidalcycles", From 3dbae7907cba29b340aea371bdaac2ba4fdec8b1 Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 13 Jun 2025 08:22:46 +0100 Subject: [PATCH 116/174] ignore .pnpm-store --- .prettierignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.prettierignore b/.prettierignore index 950e59f19..c9584aca0 100644 --- a/.prettierignore +++ b/.prettierignore @@ -13,3 +13,4 @@ pnpm-workspace.yaml website/.astro !tidal-drum-machines.json !tidal-drum-machines-alias.json +.pnpm-store From d7b83e200c01820a0733dcb99bef577a34089209 Mon Sep 17 00:00:00 2001 From: Bernhard Wagner Date: Thu, 12 Jun 2025 19:32:41 +0200 Subject: [PATCH 117/174] fix issue #1368 euclidLegatoRot --- .gitignore | 1 + packages/core/euclid.mjs | 44 ++++++----- packages/core/test/euclid.test.js | 89 +++++++++++++++++++++++ packages/core/test/util.test.mjs | 65 ++++++++++++++--- packages/core/util.mjs | 2 + pnpm-workspace.yaml | 16 ++-- test/__snapshots__/examples.test.mjs.snap | 25 ++++--- test/__snapshots__/tunes.test.mjs.snap | 24 +++--- 8 files changed, 205 insertions(+), 61 deletions(-) create mode 100644 packages/core/test/euclid.test.js diff --git a/.gitignore b/.gitignore index 59d9940e1..2be3ee698 100644 --- a/.gitignore +++ b/.gitignore @@ -130,3 +130,4 @@ fabric.properties samples/* !samples/README.md +.idea/ diff --git a/packages/core/euclid.mjs b/packages/core/euclid.mjs index 1a5be78b4..c3bb7a6d5 100644 --- a/packages/core/euclid.mjs +++ b/packages/core/euclid.mjs @@ -10,9 +10,8 @@ https://rohandrape.net/?t=hmt This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ -import { Pattern, timeCat, register, silence } from './pattern.mjs'; +import { timeCat, register, silence } from './pattern.mjs'; import { rotate, flatten, splitAt, zipWith } from './util.mjs'; -import Fraction from './fraction.mjs'; const left = function (n, x) { const [ons, offs] = n; @@ -42,29 +41,26 @@ const _bjork = function (n, x) { export const bjork = function (ons, steps) { const inverted = ons < 0; - ons = Math.abs(ons); - const offs = steps - ons; - const x = Array(ons).fill([1]); - const y = Array(offs).fill([0]); - const result = _bjork([ons, offs], [x, y]); - const p = flatten(result[1][0]).concat(flatten(result[1][1])); - if (inverted) { - return p.map((x) => (x === 0 ? 1 : 0)); - } - return p; + const absOns = Math.abs(ons); + const offs = steps - absOns; + const ones = Array(absOns).fill([1]); + const zeros = Array(offs).fill([0]); + const result = _bjork([absOns, offs], [ones, zeros]); + const pattern = flatten(result[1][0]).concat(flatten(result[1][1])); + return inverted ? pattern.map((x) => 1 - x) : pattern; }; /** - * Changes the structure of the pattern to form an euclidean rhythm. - * Euclidian rhythms are rhythms obtained using the greatest common + * Changes the structure of the pattern to form an Euclidean rhythm. + * Euclidean rhythms are rhythms obtained using the greatest common * divisor of two numbers. They were described in 2004 by Godfried - * Toussaint, a canadian computer scientist. Euclidian rhythms are + * Toussaint, a Canadian computer scientist. Euclidean rhythms are * really useful for computer/algorithmic music because they can * describe a large number of rhythms with a couple of numbers. * * @memberof Pattern * @name euclid - * @param {number} pulses the number of onsets / beats + * @param {number} pulses the number of onsets/beats * @param {number} steps the number of steps to fill * @returns Pattern * @example @@ -76,7 +72,7 @@ export const bjork = function (ons, steps) { * Like `euclid`, but has an additional parameter for 'rotating' the resulting sequence. * @memberof Pattern * @name euclidRot - * @param {number} pulses the number of onsets / beats + * @param {number} pulses the number of onsets/beats * @param {number} steps the number of steps to fill * @param {number} rotation offset in steps * @returns Pattern @@ -86,13 +82,13 @@ export const bjork = function (ons, steps) { */ /** - * @example // A thirteenth century Persian rhythm called Khafif-e-ramal. + * @example // A thirteenth-century Persian rhythm called Khafif-e-ramal. * note("c3").euclid(2,5) * @example // The archetypal pattern of the Cumbia from Colombia, as well as a Calypso rhythm from Trinidad. * note("c3").euclid(3,4) * @example // Another thirteenth century Persian rhythm by the name of Khafif-e-ramal, as well as a Rumanian folk-dance rhythm. * note("c3").euclidRot(3,5,2) - * @example // A Ruchenitza rhythm used in a Bulgarian folk-dance. + * @example // A Ruchenitza rhythm used in a Bulgarian folk dance. * note("c3").euclid(3,7) * @example // The Cuban tresillo pattern. * note("c3").euclid(3,8) @@ -151,8 +147,10 @@ export const { euclidrot, euclidRot } = register(['euclidrot', 'euclidRot'], fun * so there will be no gaps. * @name euclidLegato * @memberof Pattern - * @param {number} pulses the number of onsets / beats + * @param {number} pulses the number of onsets/beats * @param {number} steps the number of steps to fill + * @param rotation offset in steps + * @param pat * @example * note("c3").euclidLegato(3,8) */ @@ -161,13 +159,13 @@ const _euclidLegato = function (pulses, steps, rotation, pat) { if (pulses < 1) { return silence; } - const bin_pat = _euclidRot(pulses, steps, rotation); + const bin_pat = _euclidRot(pulses, steps, 0); const gapless = bin_pat .join('') .split('1') .slice(1) .map((s) => [s.length + 1, true]); - return pat.struct(timeCat(...gapless)); + return pat.struct(timeCat(...gapless)).late(rotation / steps); }; export const euclidLegato = register(['euclidLegato'], function (pulses, steps, pat) { @@ -180,7 +178,7 @@ export const euclidLegato = register(['euclidLegato'], function (pulses, steps, * the resulting sequence * @name euclidLegatoRot * @memberof Pattern - * @param {number} pulses the number of onsets / beats + * @param {number} pulses the number of onsets/beats * @param {number} steps the number of steps to fill * @param {number} rotation offset in steps * @example diff --git a/packages/core/test/euclid.test.js b/packages/core/test/euclid.test.js new file mode 100644 index 000000000..a33ec9514 --- /dev/null +++ b/packages/core/test/euclid.test.js @@ -0,0 +1,89 @@ +import { bjork } from '../euclid.mjs'; +import { describe, expect, it } from 'vitest'; +import { fastcat } from '../pattern.mjs'; + +describe('bjork', () => { + it('should apply bjorklund to ons and steps', () => { + expect(bjork(3, 8)).toStrictEqual([1, 0, 0, 1, 0, 0, 1, 0]); + expect(bjork(-3, 8)).toStrictEqual([0, 1, 1, 0, 1, 1, 0, 1]); + expect(bjork(8, 8)).toStrictEqual([1, 1, 1, 1, 1, 1, 1, 1]); + expect(bjork(-8, 8)).toStrictEqual([0, 0, 0, 0, 0, 0, 0, 0]); + expect(bjork(5, 8)).toStrictEqual([1, 0, 1, 1, 0, 1, 1, 0]); + }); +}); + +describe('euclid', () => { + it('Can create euclid', () => { + expect( + fastcat('a') + .euclid(3, 8) + .firstCycle() + .sort((a, b) => a.part.begin.sub(b.part.begin)) + .map((a) => a.showWhole(true)), + ).toStrictEqual(['0/1 → 1/8: a', '3/8 → 1/2: a', '3/4 → 7/8: a']); + expect( + fastcat('a') + .euclid(5, 8) + .firstCycle() + .sort((a, b) => a.part.begin.sub(b.part.begin)) + .map((a) => a.showWhole(true)), + ).toStrictEqual(['0/1 → 1/8: a', '1/4 → 3/8: a', '3/8 → 1/2: a', '5/8 → 3/4: a', '3/4 → 7/8: a']); + }); +}); + +describe('euclidRot', () => { + it('Can create euclidRot', () => { + expect( + fastcat('a') + .euclidRot(3, 8, 2) + .firstCycle() + .sort((a, b) => a.part.begin.sub(b.part.begin)) + .map((a) => a.showWhole(true)), + ).toStrictEqual(['0/1 → 1/8: a', '1/4 → 3/8: a', '5/8 → 3/4: a']); + expect( + fastcat('a') + .euclidRot(5, 8, 2) + .firstCycle() + .sort((a, b) => a.part.begin.sub(b.part.begin)) + .map((a) => a.showWhole(true)), + ).toStrictEqual(['0/1 → 1/8: a', '1/4 → 3/8: a', '1/2 → 5/8: a', '5/8 → 3/4: a', '7/8 → 1/1: a']); + }); +}); + +describe('euclidLegato', () => { + it('Can create euclidLegato', () => { + expect( + fastcat('a') + .euclidLegato(3, 8) + .firstCycle() + .sort((a, b) => a.part.begin.sub(b.part.begin)) + .map((a) => a.showWhole(true)), + ).toStrictEqual(['0/1 → 3/8: a', '3/8 → 3/4: a', '3/4 → 1/1: a']); + expect( + fastcat('a') + .euclidLegato(5, 8) + .firstCycle() + .sort((a, b) => a.part.begin.sub(b.part.begin)) + .map((a) => a.showWhole(true)), + ).toStrictEqual(['0/1 → 1/4: a', '1/4 → 3/8: a', '3/8 → 5/8: a', '5/8 → 3/4: a', '3/4 → 1/1: a']); + }); +}); + +describe('euclidLegatoRot', () => { + it('Can create euclidLegatoRot', () => { + expect( + fastcat('a') + .euclidLegatoRot(3, 8, 2) + .firstCycle() + .sort((a, b) => a.part.begin.sub(b.part.begin)) + .map((a) => a.showWhole(true)), + ).toStrictEqual(['0/1 → 1/4: a', '1/4 → 5/8: a', '5/8 → 1/1: a']); + expect( + fastcat('a') + .euclidLegatoRot(5, 8, 2) + .firstCycle() + .sort((a, b) => a.part.begin.sub(b.part.begin)) + .map((a) => a.showWhole(true)), + ).toStrictEqual(['0/1 → 1/4: a', '1/4 → 1/2: a', '1/2 → 5/8: a', '5/8 → 7/8: a', '7/8 → 1/1: a']); + }); +}); diff --git a/packages/core/test/util.test.mjs b/packages/core/test/util.test.mjs index 6b1053a3f..a511e0fc2 100644 --- a/packages/core/test/util.test.mjs +++ b/packages/core/test/util.test.mjs @@ -6,21 +6,25 @@ This program is free software: you can redistribute it and/or modify it under th import { pure } from '../pattern.mjs'; import { - isNote, - tokenizeNote, - noteToMidi, - midiToFreq, - freqToMidi, _mod, compose, + flatten, + fractionalArgs, + freqToMidi, getFrequency, getPlayableNoteValue, - parseNumeral, - parseFractional, + isNote, + midiToFreq, + noteToMidi, numeralArgs, - fractionalArgs, + parseFractional, + parseNumeral, + rotate, + splitAt, + tokenizeNote, + zipWith, } from '../util.mjs'; -import { describe, it, expect } from 'vitest'; +import { describe, expect, it } from 'vitest'; describe('isNote', () => { it('should recognize notes without accidentals', () => { @@ -233,3 +237,46 @@ describe('fractionalArgs', () => { expect(add('q', 2)).toBe(2.25); }); }); + +describe('rotate', () => { + it('should rotate array to the left', () => { + expect(rotate([0, 1, 2, 3], 2)).toStrictEqual([2, 3, 0, 1]); + expect(rotate([0, 1, 2, 3], 0)).toStrictEqual([0, 1, 2, 3]); + expect(rotate([0, 1, 2, 3], -3)).toStrictEqual([1, 2, 3, 0]); + expect(rotate([0, 1, 2, 3], 3)).toStrictEqual([3, 0, 1, 2]); + expect(rotate([0], 3)).toStrictEqual([0]); + expect(rotate([], 3)).toStrictEqual([]); + }); +}); + +describe('flatten', () => { + it('should flatten array by one level', () => { + expect(flatten([0, 1, 2, 3])).toStrictEqual([0, 1, 2, 3]); + expect(flatten([0, 1, [2, 3]])).toStrictEqual([0, 1, 2, 3]); + expect(flatten([0, [1, [2, 3]]])).toStrictEqual([0, 1, [2, 3]]); + expect(flatten([0])).toStrictEqual([0]); + expect(flatten([])).toStrictEqual([]); + }); +}); + +describe('splitAt', () => { + it('should split array into two', () => { + expect(splitAt(2, [0, 1, 2, 3])).toStrictEqual([ + [0, 1], + [2, 3], + ]); + expect(splitAt(0, [0, 1, 2, 3])).toStrictEqual([[], [0, 1, 2, 3]]); + expect(splitAt(-3, [0, 1, 2, 3])).toStrictEqual([[0], [1, 2, 3]]); + expect(splitAt(3, [0, 1, 2, 3])).toStrictEqual([[0, 1, 2], [3]]); + }); +}); + +describe('zipWith', () => { + it('should use the function to combine the two arrays element-wise', () => { + expect(zipWith((a, b) => a + b, [0, 1, 2, 3], [0, 1, 2, 3])).toStrictEqual([0, 2, 4, 6]); + expect(zipWith((a, b) => a + b, [0, 1, 2, 3], [0, 1, 2])).toStrictEqual([0, 2, 4, NaN]); + expect(zipWith((a, b) => a + b, [0, 1, 2], [0, 1, 2, 3])).toStrictEqual([0, 2, 4]); + expect(zipWith((a) => a, [0, 1, 2], [1, 2, 3, 0])).toStrictEqual([0, 1, 2]); + expect(zipWith((a, b) => b, [0, 1, 2], [1, 2, 3, 0])).toStrictEqual([1, 2, 3]); + }); +}); diff --git a/packages/core/util.mjs b/packages/core/util.mjs index b81811bae..756fac8e8 100644 --- a/packages/core/util.mjs +++ b/packages/core/util.mjs @@ -162,6 +162,7 @@ export const compose = (...funcs) => pipe(...funcs.reverse()); // Removes 'None' values from given list export const removeUndefineds = (xs) => xs.filter((x) => x != undefined); +// flattens by one level export const flatten = (arr) => [].concat(...arr); export const id = (a) => a; @@ -237,6 +238,7 @@ export const splitAt = function (index, value) { return [value.slice(0, index), value.slice(index)]; }; +// Uses the function f to combine the arrays xs, ys element-wise export const zipWith = (f, xs, ys) => xs.map((n, i) => f(n, ys[i])); export const pairs = function (xs) { diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index ef8cc99d5..a4fca9ff7 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,6 +1,12 @@ packages: - # all packages in direct subdirs of packages/ - - "packages/*" - - "examples/*" - - "tools/dbpatch" - - "website/" + - packages/* + - examples/* + - tools/dbpatch + - website/ + +onlyBuiltDependencies: + - esbuild + - nx + - sharp + - tree-sitter + - tree-sitter-haskell diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 0eacfdfb0..85948f0c0 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -3145,18 +3145,19 @@ exports[`runs examples > example "euclidLegato" example index 0 1`] = ` exports[`runs examples > example "euclidLegatoRot" example index 0 1`] = ` [ - "[ 0/1 → 1/4 | note:c3 ]", - "[ 1/4 → 3/4 | note:c3 ]", - "[ 3/4 → 1/1 | note:c3 ]", - "[ 1/1 → 5/4 | note:c3 ]", - "[ 5/4 → 7/4 | note:c3 ]", - "[ 7/4 → 2/1 | note:c3 ]", - "[ 2/1 → 9/4 | note:c3 ]", - "[ 9/4 → 11/4 | note:c3 ]", - "[ 11/4 → 3/1 | note:c3 ]", - "[ 3/1 → 13/4 | note:c3 ]", - "[ 13/4 → 15/4 | note:c3 ]", - "[ 15/4 → 4/1 | note:c3 ]", + "[ -1/5 ⇜ (0/1 → 1/5) | note:c3 ]", + "[ 1/5 → 2/5 | note:c3 ]", + "[ 2/5 → 4/5 | note:c3 ]", + "[ 4/5 → 6/5 | note:c3 ]", + "[ 6/5 → 7/5 | note:c3 ]", + "[ 7/5 → 9/5 | note:c3 ]", + "[ 9/5 → 11/5 | note:c3 ]", + "[ 11/5 → 12/5 | note:c3 ]", + "[ 12/5 → 14/5 | note:c3 ]", + "[ 14/5 → 16/5 | note:c3 ]", + "[ 16/5 → 17/5 | note:c3 ]", + "[ 17/5 → 19/5 | note:c3 ]", + "[ (19/5 → 4/1) ⇝ 21/5 | note:c3 ]", ] `; diff --git a/test/__snapshots__/tunes.test.mjs.snap b/test/__snapshots__/tunes.test.mjs.snap index 06267b8f9..4a0ddd539 100644 --- a/test/__snapshots__/tunes.test.mjs.snap +++ b/test/__snapshots__/tunes.test.mjs.snap @@ -7318,12 +7318,12 @@ exports[`renders tunes > tune: randomBells 1`] = ` [ "[ -9/8 ⇜ (0/1 → 3/8) | gain:0.6 note:A3 velocity:0.5989903202280402 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ -3/4 ⇜ (0/1 → 3/4) | gain:0.6 note:C5 velocity:0.8369929669424891 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 0/1 → 3/2 | note:D2 s:bass clip:1 gain:0.8 ]", + "[ 0/1 → 3/2 | note:F2 s:bass clip:1 gain:0.8 ]", "[ 0/1 → 9/4 | gain:0.6 note:D3 velocity:0.5 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 3/8 → 21/8 | gain:0.6 note:F5 velocity:0.9213038925081491 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 3/4 → 3/1 | gain:0.6 note:C5 velocity:0.8426077850162983 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 3/2 → 9/4 | note:D2 s:bass clip:1 gain:0.8 ]", - "[ 9/4 → 3/1 | note:D2 s:bass clip:1 gain:0.8 ]", + "[ 3/2 → 9/4 | note:F2 s:bass clip:1 gain:0.8 ]", + "[ 9/4 → 3/1 | note:F2 s:bass clip:1 gain:0.8 ]", "[ 9/4 → 9/2 | gain:0.6 note:D4 velocity:0.7006962578743696 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 21/8 → 39/8 | gain:0.6 note:C4 velocity:0.6507943943142891 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 3/1 → 9/2 | note:D2 s:bass clip:1 gain:0.8 ]", @@ -7335,12 +7335,12 @@ exports[`renders tunes > tune: randomBells 1`] = ` "[ (21/4 → 6/1) ⇝ 27/4 | gain:0.6 note:D4 velocity:0.6988155404105783 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 39/8 ⇜ (6/1 → 51/8) | gain:0.6 note:D5 velocity:0.8758113365620375 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 21/4 ⇜ (6/1 → 27/4) | gain:0.6 note:D4 velocity:0.6988155404105783 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 6/1 → 15/2 | note:A2 s:bass clip:1 gain:0.8 ]", + "[ 6/1 → 15/2 | note:D2 s:bass clip:1 gain:0.8 ]", "[ 6/1 → 33/4 | gain:0.6 note:G4 velocity:0.7597710825502872 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 51/8 → 69/8 | gain:0.6 note:G4 velocity:0.7743164440616965 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 27/4 → 9/1 | gain:0.6 note:C5 velocity:0.8362447572872043 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 15/2 → 33/4 | note:A2 s:bass clip:1 gain:0.8 ]", - "[ 33/4 → 9/1 | note:A2 s:bass clip:1 gain:0.8 ]", + "[ 15/2 → 33/4 | note:D2 s:bass clip:1 gain:0.8 ]", + "[ 33/4 → 9/1 | note:D2 s:bass clip:1 gain:0.8 ]", "[ 33/4 → 21/2 | gain:0.6 note:A3 velocity:0.5914018759503961 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 69/8 → 87/8 | gain:0.6 note:G4 velocity:0.754063542932272 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 9/1 → 21/2 | note:A2 s:bass clip:1 gain:0.8 ]", @@ -7352,12 +7352,12 @@ exports[`renders tunes > tune: randomBells 1`] = ` "[ (45/4 → 12/1) ⇝ 51/4 | gain:0.6 note:A4 velocity:0.7972785895690322 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 87/8 ⇜ (12/1 → 99/8) | gain:0.6 note:F4 velocity:0.7347871446982026 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 45/4 ⇜ (12/1 → 51/4) | gain:0.6 note:A4 velocity:0.7972785895690322 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 12/1 → 27/2 | note:G2 s:bass clip:1 gain:0.8 ]", + "[ 12/1 → 27/2 | note:A2 s:bass clip:1 gain:0.8 ]", "[ 12/1 → 57/4 | gain:0.6 note:G5 velocity:0.9797635599970818 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 99/8 → 117/8 | gain:0.6 note:C4 velocity:0.6662392104044557 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 51/4 → 15/1 | gain:0.6 note:F5 velocity:0.9516951469704509 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 27/2 → 57/4 | note:G2 s:bass clip:1 gain:0.8 ]", - "[ 57/4 → 15/1 | note:G2 s:bass clip:1 gain:0.8 ]", + "[ 27/2 → 57/4 | note:A2 s:bass clip:1 gain:0.8 ]", + "[ 57/4 → 15/1 | note:A2 s:bass clip:1 gain:0.8 ]", "[ 57/4 → 33/2 | gain:0.6 note:F5 velocity:0.9182533202692866 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 117/8 → 135/8 | gain:0.6 note:G3 velocity:0.5711571052670479 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 15/1 → 33/2 | note:G2 s:bass clip:1 gain:0.8 ]", @@ -7369,12 +7369,12 @@ exports[`renders tunes > tune: randomBells 1`] = ` "[ (69/4 → 18/1) ⇝ 75/4 | gain:0.6 note:F3 velocity:0.5081270858645439 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 135/8 ⇜ (18/1 → 147/8) | gain:0.6 note:F5 velocity:0.9456470254808664 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 69/4 ⇜ (18/1 → 75/4) | gain:0.6 note:F3 velocity:0.5081270858645439 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 18/1 → 39/2 | note:F2 s:bass clip:1 gain:0.8 ]", + "[ 18/1 → 39/2 | note:G2 s:bass clip:1 gain:0.8 ]", "[ 18/1 → 81/4 | gain:0.6 note:A3 velocity:0.6086445553228259 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 147/8 → 165/8 | gain:0.6 note:F3 velocity:0.5062594395130873 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 75/4 → 21/1 | gain:0.6 note:D4 velocity:0.6716219391673803 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 39/2 → 81/4 | note:F2 s:bass clip:1 gain:0.8 ]", - "[ 81/4 → 21/1 | note:F2 s:bass clip:1 gain:0.8 ]", + "[ 39/2 → 81/4 | note:G2 s:bass clip:1 gain:0.8 ]", + "[ 81/4 → 21/1 | note:G2 s:bass clip:1 gain:0.8 ]", "[ 81/4 → 45/2 | gain:0.6 note:D4 velocity:0.7043459005653858 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 165/8 → 183/8 | gain:0.6 note:D5 velocity:0.8878388572484255 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 21/1 → 45/2 | note:F2 s:bass clip:1 gain:0.8 ]", From 7f50bcebd29a36d90b13678ebc6473a512b0f291 Mon Sep 17 00:00:00 2001 From: Bernhard Wagner Date: Fri, 13 Jun 2025 10:28:03 +0200 Subject: [PATCH 118/174] avoid floating point inaccuracy by using Fraction --- packages/core/euclid.mjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/core/euclid.mjs b/packages/core/euclid.mjs index c3bb7a6d5..2ee9962da 100644 --- a/packages/core/euclid.mjs +++ b/packages/core/euclid.mjs @@ -12,6 +12,7 @@ This program is free software: you can redistribute it and/or modify it under th import { timeCat, register, silence } from './pattern.mjs'; import { rotate, flatten, splitAt, zipWith } from './util.mjs'; +import Fraction, { lcm } from './fraction.mjs'; const left = function (n, x) { const [ons, offs] = n; @@ -165,7 +166,7 @@ const _euclidLegato = function (pulses, steps, rotation, pat) { .split('1') .slice(1) .map((s) => [s.length + 1, true]); - return pat.struct(timeCat(...gapless)).late(rotation / steps); + return pat.struct(timeCat(...gapless)).late(Fraction(rotation).div(steps)); }; export const euclidLegato = register(['euclidLegato'], function (pulses, steps, pat) { From 38e7fe606c3a355277ca5dbf7ef88568f0762ca8 Mon Sep 17 00:00:00 2001 From: yaxu Date: Fri, 13 Jun 2025 16:47:40 +0200 Subject: [PATCH 119/174] Update .github/workflows/deploy.yml --- .github/workflows/deploy.yml | 34 +++++++++++----------------------- 1 file changed, 11 insertions(+), 23 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 9a853c783..669073887 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -2,13 +2,6 @@ name: Build and Deploy on: [workflow_dispatch] -# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages -permissions: - contents: read - pages: write - id-token: write - deployments: write - # Allow one concurrent deployment concurrency: group: "pages" @@ -16,10 +9,9 @@ concurrency: jobs: build: - runs-on: ubuntu-latest - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} + runs-on: docker + env: + SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 @@ -35,15 +27,11 @@ jobs: - name: Build run: pnpm build - - name: Setup Pages - uses: actions/configure-pages@v2 - - - name: Upload artifact - uses: actions/upload-pages-artifact@v3 - with: - # Upload entire repository - path: "./website/dist" - - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 + - name: Deploy + run: | + eval $(ssh-agent -s) + echo "$SSH_PRIVATE_KEY" | ssh-add - + apt update && apt install -y rsync + mkdir ~/.ssh + ssh-keyscan matrix.toplap.org > ~/.ssh/known_hosts + rsync -atv --progress ./website/dist strudel@matrix.toplap.org:/home/strudel/dist \ No newline at end of file From 8fba92f447cab4a104b694f86dd74303dcc5cfc8 Mon Sep 17 00:00:00 2001 From: yaxu Date: Fri, 13 Jun 2025 17:10:37 +0200 Subject: [PATCH 120/174] Update .github/workflows/deploy.yml --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 669073887..8eefebc2c 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -34,4 +34,4 @@ jobs: apt update && apt install -y rsync mkdir ~/.ssh ssh-keyscan matrix.toplap.org > ~/.ssh/known_hosts - rsync -atv --progress ./website/dist strudel@matrix.toplap.org:/home/strudel/dist \ No newline at end of file + rsync -atv --progress ./website/dist/ strudel@matrix.toplap.org:/home/strudel/deploy \ No newline at end of file From 3428e18e7d36142ed10acd54251cc2ccce87b6f1 Mon Sep 17 00:00:00 2001 From: yaxu Date: Fri, 13 Jun 2025 17:26:37 +0200 Subject: [PATCH 121/174] Update .github/workflows/deploy.yml --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 8eefebc2c..f82f0e30f 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -34,4 +34,4 @@ jobs: apt update && apt install -y rsync mkdir ~/.ssh ssh-keyscan matrix.toplap.org > ~/.ssh/known_hosts - rsync -atv --progress ./website/dist/ strudel@matrix.toplap.org:/home/strudel/deploy \ No newline at end of file + rsync -atv --delete --progress ./website/dist/ strudel@matrix.toplap.org:/home/strudel/deploy \ No newline at end of file From 0c193238c55217725045de3d1fdfe75a7a2636d5 Mon Sep 17 00:00:00 2001 From: yaxu Date: Fri, 13 Jun 2025 17:31:03 +0200 Subject: [PATCH 122/174] Update .github/workflows/deploy.yml --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index f82f0e30f..d8049d767 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -34,4 +34,4 @@ jobs: apt update && apt install -y rsync mkdir ~/.ssh ssh-keyscan matrix.toplap.org > ~/.ssh/known_hosts - rsync -atv --delete --progress ./website/dist/ strudel@matrix.toplap.org:/home/strudel/deploy \ No newline at end of file + rsync -atv --delete --delete-after --progress ./website/dist/ strudel@matrix.toplap.org:/home/strudel/deploy \ No newline at end of file From 47de9e45ff9e976908f2aa59a54bda211933b6cb Mon Sep 17 00:00:00 2001 From: yaxu Date: Fri, 13 Jun 2025 17:31:33 +0200 Subject: [PATCH 123/174] Update .github/workflows/deploy.yml --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index d8049d767..561fee658 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -20,7 +20,7 @@ jobs: - uses: actions/setup-node@v4 with: node-version: 20 - cache: "pnpm" + # cache: "pnpm" - name: Install Dependencies run: pnpm install From aa20526963fe216fe9ed82f5f3bda114a31f024e Mon Sep 17 00:00:00 2001 From: yaxu Date: Fri, 13 Jun 2025 17:33:47 +0200 Subject: [PATCH 124/174] Update .github/workflows/test.yml --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 498cd800b..765f5958d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,7 +17,7 @@ jobs: - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - cache: 'pnpm' + # cache: 'pnpm' - run: pnpm install - run: pnpm run format-check - run: pnpm run lint From 2bc3d69fb0e240214cd24304aef4d85d0f7d12ad Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 14 Jun 2025 14:01:11 +0200 Subject: [PATCH 125/174] replace github links in release notes --- .../content/blog/release-0.0.2-schwindlig.mdx | 52 ++-- .../blog/release-0.0.2.1-stuermisch.mdx | 80 ++--- .../content/blog/release-0.0.3-maelstrom.mdx | 104 +++---- .../src/content/blog/release-0.0.4-gischt.mdx | 62 ++-- .../content/blog/release-0.3.0-donauwelle.mdx | 74 ++--- .../content/blog/release-0.4.0-brandung.mdx | 8 +- .../src/content/blog/release-0.5.0-wirbel.mdx | 54 ++-- .../blog/release-0.6.0-zimtschnecke.mdx | 104 +++---- .../content/blog/release-0.7.0-zuckerguss.mdx | 118 +++---- .../blog/release-0.8.0-himbeermuffin.mdx | 84 ++--- .../blog/release-0.9.0-bananenbrot.mdx | 82 ++--- .../blog/release-1.0.0-geburtstagskuchen.mdx | 288 +++++++++--------- website/src/content/blog/year-2.mdx | 4 +- 13 files changed, 557 insertions(+), 557 deletions(-) diff --git a/website/src/content/blog/release-0.0.2-schwindlig.mdx b/website/src/content/blog/release-0.0.2-schwindlig.mdx index cf4bc19d6..8dbff4928 100644 --- a/website/src/content/blog/release-0.0.2-schwindlig.mdx +++ b/website/src/content/blog/release-0.0.2-schwindlig.mdx @@ -8,33 +8,33 @@ author: froos ## What's Changed -- Most work done as [commits to main](https://github.com/tidalcycles/strudel/commits/2a0d8c3f77ff7b34e82602e2d02400707f367316) -- repl + reify functions by @felixroos in https://github.com/tidalcycles/strudel/pull/2 -- Fix path by @yaxu in https://github.com/tidalcycles/strudel/pull/3 -- update readme for local dev by @kindohm in https://github.com/tidalcycles/strudel/pull/4 -- Patternify all the things by @yaxu in https://github.com/tidalcycles/strudel/pull/5 -- krill parser + improved repl by @felixroos in https://github.com/tidalcycles/strudel/pull/6 -- fixed editor crash by @felixroos in https://github.com/tidalcycles/strudel/pull/7 -- timeCat by @yaxu in https://github.com/tidalcycles/strudel/pull/8 -- Bugfix every, and create more top level functions by @yaxu in https://github.com/tidalcycles/strudel/pull/9 -- Failing test for `when` WIP by @yaxu in https://github.com/tidalcycles/strudel/pull/10 -- Added mask() and struct() by @yaxu in https://github.com/tidalcycles/strudel/pull/11 -- Add continuous signals (sine, cosine, saw, etc) by @yaxu in https://github.com/tidalcycles/strudel/pull/13 -- add apply and layer, and missing div/mul methods by @yaxu in https://github.com/tidalcycles/strudel/pull/15 -- higher latencyHint by @felixroos in https://github.com/tidalcycles/strudel/pull/16 -- test: 📦 Add missing dependency and a CI check, to prevent oversights ;p by @puria in https://github.com/tidalcycles/strudel/pull/17 -- fix: 💄 Enhance visualisation of the Tutorial on mobile by @puria in https://github.com/tidalcycles/strudel/pull/19 -- Stateful queries and events (WIP) by @yaxu in https://github.com/tidalcycles/strudel/pull/14 -- Fix resolveState by @yaxu in https://github.com/tidalcycles/strudel/pull/22 -- added \_asNumber + interpret numbers as midi by @felixroos in https://github.com/tidalcycles/strudel/pull/21 -- Update package.json by @ChiakiUehira in https://github.com/tidalcycles/strudel/pull/23 -- packaging by @felixroos in https://github.com/tidalcycles/strudel/pull/24 +- Most work done as [commits to main](https://codeberg.org/uzu/strudel/commit/2a0d8c3f77ff7b34e82602e2d02400707f367316) +- repl + reify functions by @felixroos in https://codeberg.org/uzu/strudel/pulls/2 +- Fix path by @yaxu in https://codeberg.org/uzu/strudel/pulls/3 +- update readme for local dev by @kindohm in https://codeberg.org/uzu/strudel/pulls/4 +- Patternify all the things by @yaxu in https://codeberg.org/uzu/strudel/pulls/5 +- krill parser + improved repl by @felixroos in https://codeberg.org/uzu/strudel/pulls/6 +- fixed editor crash by @felixroos in https://codeberg.org/uzu/strudel/pulls/7 +- timeCat by @yaxu in https://codeberg.org/uzu/strudel/pulls/8 +- Bugfix every, and create more top level functions by @yaxu in https://codeberg.org/uzu/strudel/pulls/9 +- Failing test for `when` WIP by @yaxu in https://codeberg.org/uzu/strudel/pulls/10 +- Added mask() and struct() by @yaxu in https://codeberg.org/uzu/strudel/pulls/11 +- Add continuous signals (sine, cosine, saw, etc) by @yaxu in https://codeberg.org/uzu/strudel/pulls/13 +- add apply and layer, and missing div/mul methods by @yaxu in https://codeberg.org/uzu/strudel/pulls/15 +- higher latencyHint by @felixroos in https://codeberg.org/uzu/strudel/pulls/16 +- test: 📦 Add missing dependency and a CI check, to prevent oversights ;p by @puria in https://codeberg.org/uzu/strudel/pulls/17 +- fix: 💄 Enhance visualisation of the Tutorial on mobile by @puria in https://codeberg.org/uzu/strudel/pulls/19 +- Stateful queries and events (WIP) by @yaxu in https://codeberg.org/uzu/strudel/pulls/14 +- Fix resolveState by @yaxu in https://codeberg.org/uzu/strudel/pulls/22 +- added \_asNumber + interpret numbers as midi by @felixroos in https://codeberg.org/uzu/strudel/pulls/21 +- Update package.json by @ChiakiUehira in https://codeberg.org/uzu/strudel/pulls/23 +- packaging by @felixroos in https://codeberg.org/uzu/strudel/pulls/24 ## New Contributors -- @felixroos made their first contribution in https://github.com/tidalcycles/strudel/pull/2 -- @kindohm made their first contribution in https://github.com/tidalcycles/strudel/pull/4 -- @puria made their first contribution in https://github.com/tidalcycles/strudel/pull/17 -- @ChiakiUehira made their first contribution in https://github.com/tidalcycles/strudel/pull/23 +- @felixroos made their first contribution in https://codeberg.org/uzu/strudel/pulls/2 +- @kindohm made their first contribution in https://codeberg.org/uzu/strudel/pulls/4 +- @puria made their first contribution in https://codeberg.org/uzu/strudel/pulls/17 +- @ChiakiUehira made their first contribution in https://codeberg.org/uzu/strudel/pulls/23 -**Full Changelog**: https://github.com/tidalcycles/strudel/commits/2a0d8c3f77ff7b34e82602e2d02400707f367316 +**Full Changelog**: https://codeberg.org/uzu/strudel/commit/2a0d8c3f77ff7b34e82602e2d02400707f367316 diff --git a/website/src/content/blog/release-0.0.2.1-stuermisch.mdx b/website/src/content/blog/release-0.0.2.1-stuermisch.mdx index edefd2b09..441d99f76 100644 --- a/website/src/content/blog/release-0.0.2.1-stuermisch.mdx +++ b/website/src/content/blog/release-0.0.2.1-stuermisch.mdx @@ -7,47 +7,47 @@ author: froos ## What's Changed -- Add chunk, chunkBack and iterBack by @yaxu in https://github.com/tidalcycles/strudel/pull/25 -- Update tutorial.mdx by @bwagner in https://github.com/tidalcycles/strudel/pull/37 -- Update tutorial.mdx by @bwagner in https://github.com/tidalcycles/strudel/pull/38 -- Compose by @felixroos in https://github.com/tidalcycles/strudel/pull/40 -- Fix polymeter by @yaxu in https://github.com/tidalcycles/strudel/pull/44 -- First run at squeezeBind, ref #32 by @yaxu in https://github.com/tidalcycles/strudel/pull/48 -- Implement `chop()` by @yaxu in https://github.com/tidalcycles/strudel/pull/50 -- OSC and SuperDirt support by @yaxu in https://github.com/tidalcycles/strudel/pull/27 -- More functions by @yaxu in https://github.com/tidalcycles/strudel/pull/56 -- More functions by @yaxu in https://github.com/tidalcycles/strudel/pull/61 -- Separate out strudel.mjs, make index.mjs aggregate module by @yaxu in https://github.com/tidalcycles/strudel/pull/62 -- Speech output by @felixroos in https://github.com/tidalcycles/strudel/pull/67 -- use new fixed version of osc-js package by @felixroos in https://github.com/tidalcycles/strudel/pull/68 -- First effort at rand() by @yaxu in https://github.com/tidalcycles/strudel/pull/69 -- More randomness, fix `rand`, and add `brand`, `irand` and `choose` by @yaxu in https://github.com/tidalcycles/strudel/pull/70 -- webaudio package by @felixroos in https://github.com/tidalcycles/strudel/pull/26 -- Port `perlin` noise, `rangex`, and `palindrome` by @yaxu in https://github.com/tidalcycles/strudel/pull/73 -- More random functions by @yaxu in https://github.com/tidalcycles/strudel/pull/74 -- Try to fix appLeft / appRight by @yaxu in https://github.com/tidalcycles/strudel/pull/75 -- Basic webserial support by @yaxu in https://github.com/tidalcycles/strudel/pull/80 -- Webaudio in REPL by @felixroos in https://github.com/tidalcycles/strudel/pull/77 -- add `striate()` by @yaxu in https://github.com/tidalcycles/strudel/pull/76 -- Tidy up a couple of old files by @mindofmatthew in https://github.com/tidalcycles/strudel/pull/84 -- Add pattern composers, implements #82 by @yaxu in https://github.com/tidalcycles/strudel/pull/83 -- Fiddles with cat/stack by @yaxu in https://github.com/tidalcycles/strudel/pull/90 -- Paper by @felixroos in https://github.com/tidalcycles/strudel/pull/98 -- Change to Affero GPL by @yaxu in https://github.com/tidalcycles/strudel/pull/101 -- Work on Codemirror 6 highlighting by @mindofmatthew in https://github.com/tidalcycles/strudel/pull/102 -- Codemirror 6 by @felixroos in https://github.com/tidalcycles/strudel/pull/97 -- Tune tests by @felixroos in https://github.com/tidalcycles/strudel/pull/104 -- /embed package: web component for repl by @felixroos in https://github.com/tidalcycles/strudel/pull/106 -- Reset, Restart and other composers by @felixroos in https://github.com/tidalcycles/strudel/pull/88 -- Embed style by @felixroos in https://github.com/tidalcycles/strudel/pull/109 -- In source doc by @yaxu in https://github.com/tidalcycles/strudel/pull/105 -- `.brak()`, `.inside()` and `.outside()` by @yaxu in https://github.com/tidalcycles/strudel/pull/112 -- loopAt by @yaxu in https://github.com/tidalcycles/strudel/pull/114 -- Osc timing improvements by @yaxu in https://github.com/tidalcycles/strudel/pull/113 +- Add chunk, chunkBack and iterBack by @yaxu in https://codeberg.org/uzu/strudel/pulls/25 +- Update tutorial.mdx by @bwagner in https://codeberg.org/uzu/strudel/pulls/37 +- Update tutorial.mdx by @bwagner in https://codeberg.org/uzu/strudel/pulls/38 +- Compose by @felixroos in https://codeberg.org/uzu/strudel/pulls/40 +- Fix polymeter by @yaxu in https://codeberg.org/uzu/strudel/pulls/44 +- First run at squeezeBind, ref #32 by @yaxu in https://codeberg.org/uzu/strudel/pulls/48 +- Implement `chop()` by @yaxu in https://codeberg.org/uzu/strudel/pulls/50 +- OSC and SuperDirt support by @yaxu in https://codeberg.org/uzu/strudel/pulls/27 +- More functions by @yaxu in https://codeberg.org/uzu/strudel/pulls/56 +- More functions by @yaxu in https://codeberg.org/uzu/strudel/pulls/61 +- Separate out strudel.mjs, make index.mjs aggregate module by @yaxu in https://codeberg.org/uzu/strudel/pulls/62 +- Speech output by @felixroos in https://codeberg.org/uzu/strudel/pulls/67 +- use new fixed version of osc-js package by @felixroos in https://codeberg.org/uzu/strudel/pulls/68 +- First effort at rand() by @yaxu in https://codeberg.org/uzu/strudel/pulls/69 +- More randomness, fix `rand`, and add `brand`, `irand` and `choose` by @yaxu in https://codeberg.org/uzu/strudel/pulls/70 +- webaudio package by @felixroos in https://codeberg.org/uzu/strudel/pulls/26 +- Port `perlin` noise, `rangex`, and `palindrome` by @yaxu in https://codeberg.org/uzu/strudel/pulls/73 +- More random functions by @yaxu in https://codeberg.org/uzu/strudel/pulls/74 +- Try to fix appLeft / appRight by @yaxu in https://codeberg.org/uzu/strudel/pulls/75 +- Basic webserial support by @yaxu in https://codeberg.org/uzu/strudel/pulls/80 +- Webaudio in REPL by @felixroos in https://codeberg.org/uzu/strudel/pulls/77 +- add `striate()` by @yaxu in https://codeberg.org/uzu/strudel/pulls/76 +- Tidy up a couple of old files by @mindofmatthew in https://codeberg.org/uzu/strudel/pulls/84 +- Add pattern composers, implements #82 by @yaxu in https://codeberg.org/uzu/strudel/pulls/83 +- Fiddles with cat/stack by @yaxu in https://codeberg.org/uzu/strudel/pulls/90 +- Paper by @felixroos in https://codeberg.org/uzu/strudel/pulls/98 +- Change to Affero GPL by @yaxu in https://codeberg.org/uzu/strudel/pulls/101 +- Work on Codemirror 6 highlighting by @mindofmatthew in https://codeberg.org/uzu/strudel/pulls/102 +- Codemirror 6 by @felixroos in https://codeberg.org/uzu/strudel/pulls/97 +- Tune tests by @felixroos in https://codeberg.org/uzu/strudel/pulls/104 +- /embed package: web component for repl by @felixroos in https://codeberg.org/uzu/strudel/pulls/106 +- Reset, Restart and other composers by @felixroos in https://codeberg.org/uzu/strudel/pulls/88 +- Embed style by @felixroos in https://codeberg.org/uzu/strudel/pulls/109 +- In source doc by @yaxu in https://codeberg.org/uzu/strudel/pulls/105 +- `.brak()`, `.inside()` and `.outside()` by @yaxu in https://codeberg.org/uzu/strudel/pulls/112 +- loopAt by @yaxu in https://codeberg.org/uzu/strudel/pulls/114 +- Osc timing improvements by @yaxu in https://codeberg.org/uzu/strudel/pulls/113 ## New Contributors -- @bwagner made their first contribution in https://github.com/tidalcycles/strudel/pull/37 -- @mindofmatthew made their first contribution in https://github.com/tidalcycles/strudel/pull/84 +- @bwagner made their first contribution in https://codeberg.org/uzu/strudel/pulls/37 +- @mindofmatthew made their first contribution in https://codeberg.org/uzu/strudel/pulls/84 -**Full Changelog**: https://github.com/tidalcycles/strudel/compare/v0.0.2...@strudel.cycles/core@0.1.0 +**Full Changelog**: https://codeberg.org/uzu/strudel/compare/v0.0.2...@strudel.cycles/core@0.1.0 diff --git a/website/src/content/blog/release-0.0.3-maelstrom.mdx b/website/src/content/blog/release-0.0.3-maelstrom.mdx index 3710a9ce5..5141b34a7 100644 --- a/website/src/content/blog/release-0.0.3-maelstrom.mdx +++ b/website/src/content/blog/release-0.0.3-maelstrom.mdx @@ -8,59 +8,59 @@ author: froos ## What's Changed -- Add chunk, chunkBack and iterBack by @yaxu in https://github.com/tidalcycles/strudel/pull/25 -- Update tutorial.mdx by @bwagner in https://github.com/tidalcycles/strudel/pull/37 -- Update tutorial.mdx by @bwagner in https://github.com/tidalcycles/strudel/pull/38 -- Compose by @felixroos in https://github.com/tidalcycles/strudel/pull/40 -- Fix polymeter by @yaxu in https://github.com/tidalcycles/strudel/pull/44 -- First run at squeezeBind, ref #32 by @yaxu in https://github.com/tidalcycles/strudel/pull/48 -- Implement `chop()` by @yaxu in https://github.com/tidalcycles/strudel/pull/50 -- OSC and SuperDirt support by @yaxu in https://github.com/tidalcycles/strudel/pull/27 -- More functions by @yaxu in https://github.com/tidalcycles/strudel/pull/56 -- More functions by @yaxu in https://github.com/tidalcycles/strudel/pull/61 -- Separate out strudel.mjs, make index.mjs aggregate module by @yaxu in https://github.com/tidalcycles/strudel/pull/62 -- Speech output by @felixroos in https://github.com/tidalcycles/strudel/pull/67 -- use new fixed version of osc-js package by @felixroos in https://github.com/tidalcycles/strudel/pull/68 -- First effort at rand() by @yaxu in https://github.com/tidalcycles/strudel/pull/69 -- More randomness, fix `rand`, and add `brand`, `irand` and `choose` by @yaxu in https://github.com/tidalcycles/strudel/pull/70 -- webaudio package by @felixroos in https://github.com/tidalcycles/strudel/pull/26 -- Port `perlin` noise, `rangex`, and `palindrome` by @yaxu in https://github.com/tidalcycles/strudel/pull/73 -- More random functions by @yaxu in https://github.com/tidalcycles/strudel/pull/74 -- Try to fix appLeft / appRight by @yaxu in https://github.com/tidalcycles/strudel/pull/75 -- Basic webserial support by @yaxu in https://github.com/tidalcycles/strudel/pull/80 -- Webaudio in REPL by @felixroos in https://github.com/tidalcycles/strudel/pull/77 -- add `striate()` by @yaxu in https://github.com/tidalcycles/strudel/pull/76 -- Tidy up a couple of old files by @mindofmatthew in https://github.com/tidalcycles/strudel/pull/84 -- Add pattern composers, implements #82 by @yaxu in https://github.com/tidalcycles/strudel/pull/83 -- Fiddles with cat/stack by @yaxu in https://github.com/tidalcycles/strudel/pull/90 -- Paper by @felixroos in https://github.com/tidalcycles/strudel/pull/98 -- Change to Affero GPL by @yaxu in https://github.com/tidalcycles/strudel/pull/101 -- Work on Codemirror 6 highlighting by @mindofmatthew in https://github.com/tidalcycles/strudel/pull/102 -- Codemirror 6 by @felixroos in https://github.com/tidalcycles/strudel/pull/97 -- Tune tests by @felixroos in https://github.com/tidalcycles/strudel/pull/104 -- /embed package: web component for repl by @felixroos in https://github.com/tidalcycles/strudel/pull/106 -- Reset, Restart and other composers by @felixroos in https://github.com/tidalcycles/strudel/pull/88 -- Embed style by @felixroos in https://github.com/tidalcycles/strudel/pull/109 -- In source doc by @yaxu in https://github.com/tidalcycles/strudel/pull/105 -- `.brak()`, `.inside()` and `.outside()` by @yaxu in https://github.com/tidalcycles/strudel/pull/112 -- loopAt by @yaxu in https://github.com/tidalcycles/strudel/pull/114 -- Osc timing improvements by @yaxu in https://github.com/tidalcycles/strudel/pull/113 -- react package + vite build by @felixroos in https://github.com/tidalcycles/strudel/pull/116 -- In source doc by @felixroos in https://github.com/tidalcycles/strudel/pull/117 -- fix: #108 by @felixroos in https://github.com/tidalcycles/strudel/pull/123 -- fix: #122 ctrl enter would add newline by @felixroos in https://github.com/tidalcycles/strudel/pull/124 -- Webdirt by @felixroos in https://github.com/tidalcycles/strudel/pull/121 -- Fix link to contributing to tutorial docs by @stephendwolff in https://github.com/tidalcycles/strudel/pull/129 -- Pianoroll enhancements by @felixroos in https://github.com/tidalcycles/strudel/pull/131 -- add createParam + createParams by @felixroos in https://github.com/tidalcycles/strudel/pull/110 -- remove cycle + delta from onTrigger by @felixroos in https://github.com/tidalcycles/strudel/pull/135 -- Scheduler improvements by @felixroos in https://github.com/tidalcycles/strudel/pull/134 -- add onTrigger helper by @felixroos in https://github.com/tidalcycles/strudel/pull/136 +- Add chunk, chunkBack and iterBack by @yaxu in https://codeberg.org/uzu/strudel/pulls/25 +- Update tutorial.mdx by @bwagner in https://codeberg.org/uzu/strudel/pulls/37 +- Update tutorial.mdx by @bwagner in https://codeberg.org/uzu/strudel/pulls/38 +- Compose by @felixroos in https://codeberg.org/uzu/strudel/pulls/40 +- Fix polymeter by @yaxu in https://codeberg.org/uzu/strudel/pulls/44 +- First run at squeezeBind, ref #32 by @yaxu in https://codeberg.org/uzu/strudel/pulls/48 +- Implement `chop()` by @yaxu in https://codeberg.org/uzu/strudel/pulls/50 +- OSC and SuperDirt support by @yaxu in https://codeberg.org/uzu/strudel/pulls/27 +- More functions by @yaxu in https://codeberg.org/uzu/strudel/pulls/56 +- More functions by @yaxu in https://codeberg.org/uzu/strudel/pulls/61 +- Separate out strudel.mjs, make index.mjs aggregate module by @yaxu in https://codeberg.org/uzu/strudel/pulls/62 +- Speech output by @felixroos in https://codeberg.org/uzu/strudel/pulls/67 +- use new fixed version of osc-js package by @felixroos in https://codeberg.org/uzu/strudel/pulls/68 +- First effort at rand() by @yaxu in https://codeberg.org/uzu/strudel/pulls/69 +- More randomness, fix `rand`, and add `brand`, `irand` and `choose` by @yaxu in https://codeberg.org/uzu/strudel/pulls/70 +- webaudio package by @felixroos in https://codeberg.org/uzu/strudel/pulls/26 +- Port `perlin` noise, `rangex`, and `palindrome` by @yaxu in https://codeberg.org/uzu/strudel/pulls/73 +- More random functions by @yaxu in https://codeberg.org/uzu/strudel/pulls/74 +- Try to fix appLeft / appRight by @yaxu in https://codeberg.org/uzu/strudel/pulls/75 +- Basic webserial support by @yaxu in https://codeberg.org/uzu/strudel/pulls/80 +- Webaudio in REPL by @felixroos in https://codeberg.org/uzu/strudel/pulls/77 +- add `striate()` by @yaxu in https://codeberg.org/uzu/strudel/pulls/76 +- Tidy up a couple of old files by @mindofmatthew in https://codeberg.org/uzu/strudel/pulls/84 +- Add pattern composers, implements #82 by @yaxu in https://codeberg.org/uzu/strudel/pulls/83 +- Fiddles with cat/stack by @yaxu in https://codeberg.org/uzu/strudel/pulls/90 +- Paper by @felixroos in https://codeberg.org/uzu/strudel/pulls/98 +- Change to Affero GPL by @yaxu in https://codeberg.org/uzu/strudel/pulls/101 +- Work on Codemirror 6 highlighting by @mindofmatthew in https://codeberg.org/uzu/strudel/pulls/102 +- Codemirror 6 by @felixroos in https://codeberg.org/uzu/strudel/pulls/97 +- Tune tests by @felixroos in https://codeberg.org/uzu/strudel/pulls/104 +- /embed package: web component for repl by @felixroos in https://codeberg.org/uzu/strudel/pulls/106 +- Reset, Restart and other composers by @felixroos in https://codeberg.org/uzu/strudel/pulls/88 +- Embed style by @felixroos in https://codeberg.org/uzu/strudel/pulls/109 +- In source doc by @yaxu in https://codeberg.org/uzu/strudel/pulls/105 +- `.brak()`, `.inside()` and `.outside()` by @yaxu in https://codeberg.org/uzu/strudel/pulls/112 +- loopAt by @yaxu in https://codeberg.org/uzu/strudel/pulls/114 +- Osc timing improvements by @yaxu in https://codeberg.org/uzu/strudel/pulls/113 +- react package + vite build by @felixroos in https://codeberg.org/uzu/strudel/pulls/116 +- In source doc by @felixroos in https://codeberg.org/uzu/strudel/pulls/117 +- fix: #108 by @felixroos in https://codeberg.org/uzu/strudel/pulls/123 +- fix: #122 ctrl enter would add newline by @felixroos in https://codeberg.org/uzu/strudel/pulls/124 +- Webdirt by @felixroos in https://codeberg.org/uzu/strudel/pulls/121 +- Fix link to contributing to tutorial docs by @stephendwolff in https://codeberg.org/uzu/strudel/pulls/129 +- Pianoroll enhancements by @felixroos in https://codeberg.org/uzu/strudel/pulls/131 +- add createParam + createParams by @felixroos in https://codeberg.org/uzu/strudel/pulls/110 +- remove cycle + delta from onTrigger by @felixroos in https://codeberg.org/uzu/strudel/pulls/135 +- Scheduler improvements by @felixroos in https://codeberg.org/uzu/strudel/pulls/134 +- add onTrigger helper by @felixroos in https://codeberg.org/uzu/strudel/pulls/136 ## New Contributors -- @bwagner made their first contribution in https://github.com/tidalcycles/strudel/pull/37 -- @mindofmatthew made their first contribution in https://github.com/tidalcycles/strudel/pull/84 -- @stephendwolff made their first contribution in https://github.com/tidalcycles/strudel/pull/129 +- @bwagner made their first contribution in https://codeberg.org/uzu/strudel/pulls/37 +- @mindofmatthew made their first contribution in https://codeberg.org/uzu/strudel/pulls/84 +- @stephendwolff made their first contribution in https://codeberg.org/uzu/strudel/pulls/129 -**Full Changelog**: https://github.com/tidalcycles/strudel/compare/v0.0.2...v0.0.3 +**Full Changelog**: https://codeberg.org/uzu/strudel/compare/v0.0.2...v0.0.3 diff --git a/website/src/content/blog/release-0.0.4-gischt.mdx b/website/src/content/blog/release-0.0.4-gischt.mdx index 9149137ca..f06dd38e8 100644 --- a/website/src/content/blog/release-0.0.4-gischt.mdx +++ b/website/src/content/blog/release-0.0.4-gischt.mdx @@ -8,38 +8,38 @@ author: froos ## What's Changed -- Webaudio rewrite by @felixroos in https://github.com/tidalcycles/strudel/pull/138 -- Fix createParam() by @yaxu in https://github.com/tidalcycles/strudel/pull/140 -- Soundfont Support by @felixroos in https://github.com/tidalcycles/strudel/pull/139 -- Serial twiddles by @yaxu in https://github.com/tidalcycles/strudel/pull/141 -- Pianoroll Object Support by @felixroos in https://github.com/tidalcycles/strudel/pull/142 -- flash effect on ctrl enter by @felixroos in https://github.com/tidalcycles/strudel/pull/144 -- can now generate short link for sharing by @felixroos in https://github.com/tidalcycles/strudel/pull/146 -- Sampler optimizations and more by @felixroos in https://github.com/tidalcycles/strudel/pull/148 -- Final update to demo.pdf by @yaxu in https://github.com/tidalcycles/strudel/pull/151 -- add webdirt drum samples to prebake for general availability by @larkob in https://github.com/tidalcycles/strudel/pull/150 -- update to tutorial documentation by @larkob in https://github.com/tidalcycles/strudel/pull/162 -- add chooseInWith/chooseCycles by @yaxu in https://github.com/tidalcycles/strudel/pull/166 -- fix: jsdoc comments by @felixroos in https://github.com/tidalcycles/strudel/pull/169 -- Pianoroll fixes by @felixroos in https://github.com/tidalcycles/strudel/pull/163 -- Talk fixes by @felixroos in https://github.com/tidalcycles/strudel/pull/164 -- Amend shapeshifter to allow use of dynamic import by @debrisapron in https://github.com/tidalcycles/strudel/pull/171 -- add more shapeshifter flags by @felixroos in https://github.com/tidalcycles/strudel/pull/99 -- Replace react-codemirror6 with @uiw/react-codemirror by @felixroos in https://github.com/tidalcycles/strudel/pull/173 -- fix some annoying bugs by @felixroos in https://github.com/tidalcycles/strudel/pull/177 -- incorporate elements of randomness to the mini notation by @bpow in https://github.com/tidalcycles/strudel/pull/165 -- replace mocha with vitest by @felixroos in https://github.com/tidalcycles/strudel/pull/175 -- scheduler improvements by @felixroos in https://github.com/tidalcycles/strudel/pull/181 -- Fix codemirror bug by @felixroos in https://github.com/tidalcycles/strudel/pull/186 -- wait for prebake to finish before evaluating by @felixroos in https://github.com/tidalcycles/strudel/pull/189 -- fix regression: old way of setting frequencies was broken by @felixroos in https://github.com/tidalcycles/strudel/pull/190 -- Soundfont file support by @felixroos in https://github.com/tidalcycles/strudel/pull/183 -- change "stride"/"offset" of successive degradeBy/chooseIn by @bpow in https://github.com/tidalcycles/strudel/pull/185 +- Webaudio rewrite by @felixroos in https://codeberg.org/uzu/strudel/pulls/138 +- Fix createParam() by @yaxu in https://codeberg.org/uzu/strudel/pulls/140 +- Soundfont Support by @felixroos in https://codeberg.org/uzu/strudel/pulls/139 +- Serial twiddles by @yaxu in https://codeberg.org/uzu/strudel/pulls/141 +- Pianoroll Object Support by @felixroos in https://codeberg.org/uzu/strudel/pulls/142 +- flash effect on ctrl enter by @felixroos in https://codeberg.org/uzu/strudel/pulls/144 +- can now generate short link for sharing by @felixroos in https://codeberg.org/uzu/strudel/pulls/146 +- Sampler optimizations and more by @felixroos in https://codeberg.org/uzu/strudel/pulls/148 +- Final update to demo.pdf by @yaxu in https://codeberg.org/uzu/strudel/pulls/151 +- add webdirt drum samples to prebake for general availability by @larkob in https://codeberg.org/uzu/strudel/pulls/150 +- update to tutorial documentation by @larkob in https://codeberg.org/uzu/strudel/pulls/162 +- add chooseInWith/chooseCycles by @yaxu in https://codeberg.org/uzu/strudel/pulls/166 +- fix: jsdoc comments by @felixroos in https://codeberg.org/uzu/strudel/pulls/169 +- Pianoroll fixes by @felixroos in https://codeberg.org/uzu/strudel/pulls/163 +- Talk fixes by @felixroos in https://codeberg.org/uzu/strudel/pulls/164 +- Amend shapeshifter to allow use of dynamic import by @debrisapron in https://codeberg.org/uzu/strudel/pulls/171 +- add more shapeshifter flags by @felixroos in https://codeberg.org/uzu/strudel/pulls/99 +- Replace react-codemirror6 with @uiw/react-codemirror by @felixroos in https://codeberg.org/uzu/strudel/pulls/173 +- fix some annoying bugs by @felixroos in https://codeberg.org/uzu/strudel/pulls/177 +- incorporate elements of randomness to the mini notation by @bpow in https://codeberg.org/uzu/strudel/pulls/165 +- replace mocha with vitest by @felixroos in https://codeberg.org/uzu/strudel/pulls/175 +- scheduler improvements by @felixroos in https://codeberg.org/uzu/strudel/pulls/181 +- Fix codemirror bug by @felixroos in https://codeberg.org/uzu/strudel/pulls/186 +- wait for prebake to finish before evaluating by @felixroos in https://codeberg.org/uzu/strudel/pulls/189 +- fix regression: old way of setting frequencies was broken by @felixroos in https://codeberg.org/uzu/strudel/pulls/190 +- Soundfont file support by @felixroos in https://codeberg.org/uzu/strudel/pulls/183 +- change "stride"/"offset" of successive degradeBy/chooseIn by @bpow in https://codeberg.org/uzu/strudel/pulls/185 ## New Contributors -- @larkob made their first contribution in https://github.com/tidalcycles/strudel/pull/150 -- @debrisapron made their first contribution in https://github.com/tidalcycles/strudel/pull/171 -- @bpow made their first contribution in https://github.com/tidalcycles/strudel/pull/165 +- @larkob made their first contribution in https://codeberg.org/uzu/strudel/pulls/150 +- @debrisapron made their first contribution in https://codeberg.org/uzu/strudel/pulls/171 +- @bpow made their first contribution in https://codeberg.org/uzu/strudel/pulls/165 -**Full Changelog**: https://github.com/tidalcycles/strudel/compare/v0.0.3...v0.0.4 +**Full Changelog**: https://codeberg.org/uzu/strudel/compare/v0.0.3...v0.0.4 diff --git a/website/src/content/blog/release-0.3.0-donauwelle.mdx b/website/src/content/blog/release-0.3.0-donauwelle.mdx index 054b36b31..a551dc53b 100644 --- a/website/src/content/blog/release-0.3.0-donauwelle.mdx +++ b/website/src/content/blog/release-0.3.0-donauwelle.mdx @@ -22,44 +22,44 @@ author: froos ## What's Changed -- Fix numbers in sampler by @felixroos in https://github.com/tidalcycles/strudel/pull/196 -- document random functions by @felixroos in https://github.com/tidalcycles/strudel/pull/199 -- add rollup-plugin-visualizer to build by @felixroos in https://github.com/tidalcycles/strudel/pull/200 -- add vowel to .out by @felixroos in https://github.com/tidalcycles/strudel/pull/201 -- Coarse crush shape by @felixroos in https://github.com/tidalcycles/strudel/pull/205 -- Webaudio guide by @felixroos in https://github.com/tidalcycles/strudel/pull/207 -- Even more docs by @felixroos in https://github.com/tidalcycles/strudel/pull/212 -- Just another docs PR by @felixroos in https://github.com/tidalcycles/strudel/pull/215 -- sampler features + fixes by @felixroos in https://github.com/tidalcycles/strudel/pull/217 -- samples now have envelopes by @felixroos in https://github.com/tidalcycles/strudel/pull/218 -- encapsulate webaudio output by @felixroos in https://github.com/tidalcycles/strudel/pull/219 -- Fix squeeze join by @yaxu in https://github.com/tidalcycles/strudel/pull/220 -- Feedback Delay by @felixroos in https://github.com/tidalcycles/strudel/pull/213 -- support negative speeds by @felixroos in https://github.com/tidalcycles/strudel/pull/222 -- focus tweak for squeezeJoin - another go at fixing #216 by @yaxu in https://github.com/tidalcycles/strudel/pull/221 -- Reverb by @felixroos in https://github.com/tidalcycles/strudel/pull/224 -- fix fastgap for events that go across cycle boundaries by @yaxu in https://github.com/tidalcycles/strudel/pull/225 -- Core util tests by @mystery-house in https://github.com/tidalcycles/strudel/pull/226 -- Refactor tunes away from tone by @felixroos in https://github.com/tidalcycles/strudel/pull/230 -- Just another docs branch by @felixroos in https://github.com/tidalcycles/strudel/pull/228 -- Patternify range by @yaxu in https://github.com/tidalcycles/strudel/pull/231 -- Out by default by @felixroos in https://github.com/tidalcycles/strudel/pull/232 -- Fix zero length queries WIP by @yaxu in https://github.com/tidalcycles/strudel/pull/234 -- add vcsl sample library by @felixroos in https://github.com/tidalcycles/strudel/pull/235 -- fx on stereo speakers by @felixroos in https://github.com/tidalcycles/strudel/pull/236 -- Tidal drum machines by @felixroos in https://github.com/tidalcycles/strudel/pull/237 -- Object arithmetic by @felixroos in https://github.com/tidalcycles/strudel/pull/238 -- Load samples from url by @felixroos in https://github.com/tidalcycles/strudel/pull/239 -- feat: support github: links by @felixroos in https://github.com/tidalcycles/strudel/pull/240 -- in source example tests by @felixroos in https://github.com/tidalcycles/strudel/pull/242 -- Readme + TLC by @felixroos in https://github.com/tidalcycles/strudel/pull/244 -- patchday by @felixroos in https://github.com/tidalcycles/strudel/pull/246 -- Some tunes by @felixroos in https://github.com/tidalcycles/strudel/pull/247 -- snapshot tests on shared snippets by @felixroos in https://github.com/tidalcycles/strudel/pull/243 -- General purpose scheduler by @felixroos in https://github.com/tidalcycles/strudel/pull/248 +- Fix numbers in sampler by @felixroos in https://codeberg.org/uzu/strudel/pulls/196 +- document random functions by @felixroos in https://codeberg.org/uzu/strudel/pulls/199 +- add rollup-plugin-visualizer to build by @felixroos in https://codeberg.org/uzu/strudel/pulls/200 +- add vowel to .out by @felixroos in https://codeberg.org/uzu/strudel/pulls/201 +- Coarse crush shape by @felixroos in https://codeberg.org/uzu/strudel/pulls/205 +- Webaudio guide by @felixroos in https://codeberg.org/uzu/strudel/pulls/207 +- Even more docs by @felixroos in https://codeberg.org/uzu/strudel/pulls/212 +- Just another docs PR by @felixroos in https://codeberg.org/uzu/strudel/pulls/215 +- sampler features + fixes by @felixroos in https://codeberg.org/uzu/strudel/pulls/217 +- samples now have envelopes by @felixroos in https://codeberg.org/uzu/strudel/pulls/218 +- encapsulate webaudio output by @felixroos in https://codeberg.org/uzu/strudel/pulls/219 +- Fix squeeze join by @yaxu in https://codeberg.org/uzu/strudel/pulls/220 +- Feedback Delay by @felixroos in https://codeberg.org/uzu/strudel/pulls/213 +- support negative speeds by @felixroos in https://codeberg.org/uzu/strudel/pulls/222 +- focus tweak for squeezeJoin - another go at fixing #216 by @yaxu in https://codeberg.org/uzu/strudel/pulls/221 +- Reverb by @felixroos in https://codeberg.org/uzu/strudel/pulls/224 +- fix fastgap for events that go across cycle boundaries by @yaxu in https://codeberg.org/uzu/strudel/pulls/225 +- Core util tests by @mystery-house in https://codeberg.org/uzu/strudel/pulls/226 +- Refactor tunes away from tone by @felixroos in https://codeberg.org/uzu/strudel/pulls/230 +- Just another docs branch by @felixroos in https://codeberg.org/uzu/strudel/pulls/228 +- Patternify range by @yaxu in https://codeberg.org/uzu/strudel/pulls/231 +- Out by default by @felixroos in https://codeberg.org/uzu/strudel/pulls/232 +- Fix zero length queries WIP by @yaxu in https://codeberg.org/uzu/strudel/pulls/234 +- add vcsl sample library by @felixroos in https://codeberg.org/uzu/strudel/pulls/235 +- fx on stereo speakers by @felixroos in https://codeberg.org/uzu/strudel/pulls/236 +- Tidal drum machines by @felixroos in https://codeberg.org/uzu/strudel/pulls/237 +- Object arithmetic by @felixroos in https://codeberg.org/uzu/strudel/pulls/238 +- Load samples from url by @felixroos in https://codeberg.org/uzu/strudel/pulls/239 +- feat: support github: links by @felixroos in https://codeberg.org/uzu/strudel/pulls/240 +- in source example tests by @felixroos in https://codeberg.org/uzu/strudel/pulls/242 +- Readme + TLC by @felixroos in https://codeberg.org/uzu/strudel/pulls/244 +- patchday by @felixroos in https://codeberg.org/uzu/strudel/pulls/246 +- Some tunes by @felixroos in https://codeberg.org/uzu/strudel/pulls/247 +- snapshot tests on shared snippets by @felixroos in https://codeberg.org/uzu/strudel/pulls/243 +- General purpose scheduler by @felixroos in https://codeberg.org/uzu/strudel/pulls/248 ## New Contributors -- @mystery-house made their first contribution in https://github.com/tidalcycles/strudel/pull/226 +- @mystery-house made their first contribution in https://codeberg.org/uzu/strudel/pulls/226 -**Full Changelog**: https://github.com/tidalcycles/strudel/compare/v0.0.4...v0.3.0 +**Full Changelog**: https://codeberg.org/uzu/strudel/compare/v0.0.4...v0.3.0 diff --git a/website/src/content/blog/release-0.4.0-brandung.mdx b/website/src/content/blog/release-0.4.0-brandung.mdx index 06023662b..01e2f9b89 100644 --- a/website/src/content/blog/release-0.4.0-brandung.mdx +++ b/website/src/content/blog/release-0.4.0-brandung.mdx @@ -8,8 +8,8 @@ author: froos ## What's Changed -- new transpiler based on acorn by @felixroos in https://github.com/tidalcycles/strudel/pull/249 -- Webaudio build by @felixroos in https://github.com/tidalcycles/strudel/pull/250 -- Repl refactoring by @felixroos in https://github.com/tidalcycles/strudel/pull/255 +- new transpiler based on acorn by @felixroos in https://codeberg.org/uzu/strudel/pulls/249 +- Webaudio build by @felixroos in https://codeberg.org/uzu/strudel/pulls/250 +- Repl refactoring by @felixroos in https://codeberg.org/uzu/strudel/pulls/255 -**Full Changelog**: https://github.com/tidalcycles/strudel/compare/v0.3.0...v0.4.0 +**Full Changelog**: https://codeberg.org/uzu/strudel/compare/v0.3.0...v0.4.0 diff --git a/website/src/content/blog/release-0.5.0-wirbel.mdx b/website/src/content/blog/release-0.5.0-wirbel.mdx index 032639fd9..3c1e1bfe2 100644 --- a/website/src/content/blog/release-0.5.0-wirbel.mdx +++ b/website/src/content/blog/release-0.5.0-wirbel.mdx @@ -26,34 +26,34 @@ author: froos ## What's Changed -- Binaries by @felixroos in https://github.com/tidalcycles/strudel/pull/254 -- fix tutorial bugs by @felixroos in https://github.com/tidalcycles/strudel/pull/263 -- fix performance bottleneck by @felixroos in https://github.com/tidalcycles/strudel/pull/266 -- Tidying up core by @yaxu in https://github.com/tidalcycles/strudel/pull/256 -- tonal update with fixed memory leak by @felixroos in https://github.com/tidalcycles/strudel/pull/272 -- add eslint by @felixroos in https://github.com/tidalcycles/strudel/pull/271 -- release version bumps by @felixroos in https://github.com/tidalcycles/strudel/pull/273 -- Support sending CRC16 bytes with serial messages by @yaxu in https://github.com/tidalcycles/strudel/pull/276 -- add licenses / credits to all tunes + remove some by @felixroos in https://github.com/tidalcycles/strudel/pull/277 -- add basic csound output by @felixroos in https://github.com/tidalcycles/strudel/pull/275 -- do not recompile orc by @felixroos in https://github.com/tidalcycles/strudel/pull/278 -- implement collect + arp function by @felixroos in https://github.com/tidalcycles/strudel/pull/281 -- Switch 'operators' from .whatHow to .what.how by @yaxu in https://github.com/tidalcycles/strudel/pull/285 -- Fancy hap show, include part in snapshots by @yaxu in https://github.com/tidalcycles/strudel/pull/291 -- Reorganise pattern.mjs with a 'toplevel first' regime by @yaxu in https://github.com/tidalcycles/strudel/pull/286 -- add prettier task by @felixroos in https://github.com/tidalcycles/strudel/pull/296 -- Move stuff to new register function by @felixroos in https://github.com/tidalcycles/strudel/pull/295 -- can now add bare numbers to numeral object props by @felixroos in https://github.com/tidalcycles/strudel/pull/287 -- update vitest by @felixroos in https://github.com/tidalcycles/strudel/pull/297 -- remove whitespace from highlighted region by @felixroos in https://github.com/tidalcycles/strudel/pull/298 -- .defragmentHaps() for merging touching haps that share a whole and value by @yaxu in https://github.com/tidalcycles/strudel/pull/299 -- fix whitespace trimming by @felixroos in https://github.com/tidalcycles/strudel/pull/300 -- add freq support to sampler by @felixroos in https://github.com/tidalcycles/strudel/pull/301 -- add lint + prettier check before test by @felixroos in https://github.com/tidalcycles/strudel/pull/305 -- Updated csoundm to use the register facility . by @gogins in https://github.com/tidalcycles/strudel/pull/303 +- Binaries by @felixroos in https://codeberg.org/uzu/strudel/pulls/254 +- fix tutorial bugs by @felixroos in https://codeberg.org/uzu/strudel/pulls/263 +- fix performance bottleneck by @felixroos in https://codeberg.org/uzu/strudel/pulls/266 +- Tidying up core by @yaxu in https://codeberg.org/uzu/strudel/pulls/256 +- tonal update with fixed memory leak by @felixroos in https://codeberg.org/uzu/strudel/pulls/272 +- add eslint by @felixroos in https://codeberg.org/uzu/strudel/pulls/271 +- release version bumps by @felixroos in https://codeberg.org/uzu/strudel/pulls/273 +- Support sending CRC16 bytes with serial messages by @yaxu in https://codeberg.org/uzu/strudel/pulls/276 +- add licenses / credits to all tunes + remove some by @felixroos in https://codeberg.org/uzu/strudel/pulls/277 +- add basic csound output by @felixroos in https://codeberg.org/uzu/strudel/pulls/275 +- do not recompile orc by @felixroos in https://codeberg.org/uzu/strudel/pulls/278 +- implement collect + arp function by @felixroos in https://codeberg.org/uzu/strudel/pulls/281 +- Switch 'operators' from .whatHow to .what.how by @yaxu in https://codeberg.org/uzu/strudel/pulls/285 +- Fancy hap show, include part in snapshots by @yaxu in https://codeberg.org/uzu/strudel/pulls/291 +- Reorganise pattern.mjs with a 'toplevel first' regime by @yaxu in https://codeberg.org/uzu/strudel/pulls/286 +- add prettier task by @felixroos in https://codeberg.org/uzu/strudel/pulls/296 +- Move stuff to new register function by @felixroos in https://codeberg.org/uzu/strudel/pulls/295 +- can now add bare numbers to numeral object props by @felixroos in https://codeberg.org/uzu/strudel/pulls/287 +- update vitest by @felixroos in https://codeberg.org/uzu/strudel/pulls/297 +- remove whitespace from highlighted region by @felixroos in https://codeberg.org/uzu/strudel/pulls/298 +- .defragmentHaps() for merging touching haps that share a whole and value by @yaxu in https://codeberg.org/uzu/strudel/pulls/299 +- fix whitespace trimming by @felixroos in https://codeberg.org/uzu/strudel/pulls/300 +- add freq support to sampler by @felixroos in https://codeberg.org/uzu/strudel/pulls/301 +- add lint + prettier check before test by @felixroos in https://codeberg.org/uzu/strudel/pulls/305 +- Updated csoundm to use the register facility . by @gogins in https://codeberg.org/uzu/strudel/pulls/303 ## New Contributors -- @gogins made their first contribution in https://github.com/tidalcycles/strudel/pull/303 +- @gogins made their first contribution in https://codeberg.org/uzu/strudel/pulls/303 -**Full Changelog**: https://github.com/tidalcycles/strudel/compare/v0.4.0...v0.5.0 +**Full Changelog**: https://codeberg.org/uzu/strudel/compare/v0.4.0...v0.5.0 diff --git a/website/src/content/blog/release-0.6.0-zimtschnecke.mdx b/website/src/content/blog/release-0.6.0-zimtschnecke.mdx index 2736974bf..19536581d 100644 --- a/website/src/content/blog/release-0.6.0-zimtschnecke.mdx +++ b/website/src/content/blog/release-0.6.0-zimtschnecke.mdx @@ -26,59 +26,59 @@ author: froos ## What's Changed -- support freq in pianoroll by @felixroos in https://github.com/tidalcycles/strudel/pull/308 -- ICLC2023 paper WIP by @yaxu in https://github.com/tidalcycles/strudel/pull/306 -- fix: copy share link to clipboard was broken for some browsers by @felixroos in https://github.com/tidalcycles/strudel/pull/311 -- Jsdoc component by @felixroos in https://github.com/tidalcycles/strudel/pull/312 -- object support for .scale by @felixroos in https://github.com/tidalcycles/strudel/pull/307 -- Astro build by @felixroos in https://github.com/tidalcycles/strudel/pull/315 -- Reference tab sort by @felixroos in https://github.com/tidalcycles/strudel/pull/318 -- tutorial updates by @jarmitage in https://github.com/tidalcycles/strudel/pull/320 -- support notes without octave by @felixroos in https://github.com/tidalcycles/strudel/pull/323 -- mini repl improvements by @felixroos in https://github.com/tidalcycles/strudel/pull/324 -- fix: workaround Object.assign globalThis by @felixroos in https://github.com/tidalcycles/strudel/pull/326 -- add examples route by @felixroos in https://github.com/tidalcycles/strudel/pull/327 -- add my-patterns by @felixroos in https://github.com/tidalcycles/strudel/pull/328 -- my-patterns build + deploy by @felixroos in https://github.com/tidalcycles/strudel/pull/329 -- my-patterns: fix paths + update readme by @felixroos in https://github.com/tidalcycles/strudel/pull/330 -- improve displaying 's' in pianoroll by @felixroos in https://github.com/tidalcycles/strudel/pull/331 -- fix: can now multiply floats in mini notation by @felixroos in https://github.com/tidalcycles/strudel/pull/332 -- Embed mode improvements by @felixroos in https://github.com/tidalcycles/strudel/pull/333 -- testing + docs docs by @felixroos in https://github.com/tidalcycles/strudel/pull/334 -- animate mvp by @felixroos in https://github.com/tidalcycles/strudel/pull/335 -- Tidy parser, implement polymeters by @yaxu in https://github.com/tidalcycles/strudel/pull/336 -- animation options by @felixroos in https://github.com/tidalcycles/strudel/pull/337 -- move /my-patterns to /swatch by @yaxu in https://github.com/tidalcycles/strudel/pull/338 -- more animate functions + mini repl fix by @felixroos in https://github.com/tidalcycles/strudel/pull/340 -- Patternify euclid, fast, slow and polymeter step parameters in mininotation by @yaxu in https://github.com/tidalcycles/strudel/pull/341 -- fixes #346 by @felixroos in https://github.com/tidalcycles/strudel/pull/347 -- Fix prebake base path by @felixroos in https://github.com/tidalcycles/strudel/pull/345 -- Fix Bjorklund by @yaxu in https://github.com/tidalcycles/strudel/pull/343 -- docs: tidal comparison + add global fx + add missing sampler fx by @felixroos in https://github.com/tidalcycles/strudel/pull/356 -- Fix .out(), renaming webaudio's out() to webaudio() by @yaxu in https://github.com/tidalcycles/strudel/pull/361 -- Support for multiple mininotation operators by @yaxu in https://github.com/tidalcycles/strudel/pull/350 -- doc structuring by @felixroos in https://github.com/tidalcycles/strudel/pull/360 -- add https to url by @urswilke in https://github.com/tidalcycles/strudel/pull/364 -- document more functions + change arp join by @felixroos in https://github.com/tidalcycles/strudel/pull/369 -- improve new draw logic by @felixroos in https://github.com/tidalcycles/strudel/pull/372 -- Draw fixes by @felixroos in https://github.com/tidalcycles/strudel/pull/377 -- update my-patterns instructions by @felixroos in https://github.com/tidalcycles/strudel/pull/384 -- docs: use note instead of n to mitigate confusion by @felixroos in https://github.com/tidalcycles/strudel/pull/385 -- add run + test + docs by @felixroos in https://github.com/tidalcycles/strudel/pull/386 -- Rename a to angle by @felixroos in https://github.com/tidalcycles/strudel/pull/387 -- document csound by @felixroos in https://github.com/tidalcycles/strudel/pull/391 -- Notes are not essential :) by @yaxu in https://github.com/tidalcycles/strudel/pull/393 -- add ribbon + test + docs by @felixroos in https://github.com/tidalcycles/strudel/pull/388 -- Add tidal-drum-patterns to examples by @urswilke in https://github.com/tidalcycles/strudel/pull/379 -- add pattern methods hurry, press and pressBy by @yaxu in https://github.com/tidalcycles/strudel/pull/397 -- proper builds + use pnpm workspaces by @felixroos in https://github.com/tidalcycles/strudel/pull/396 -- fix: minirepl styles by @felixroos in https://github.com/tidalcycles/strudel/pull/398 -- can now await initAudio + initAudioOnFirstClick by @felixroos in https://github.com/tidalcycles/strudel/pull/399 -- release webaudio by @felixroos in https://github.com/tidalcycles/strudel/pull/400 +- support freq in pianoroll by @felixroos in https://codeberg.org/uzu/strudel/pulls/308 +- ICLC2023 paper WIP by @yaxu in https://codeberg.org/uzu/strudel/pulls/306 +- fix: copy share link to clipboard was broken for some browsers by @felixroos in https://codeberg.org/uzu/strudel/pulls/311 +- Jsdoc component by @felixroos in https://codeberg.org/uzu/strudel/pulls/312 +- object support for .scale by @felixroos in https://codeberg.org/uzu/strudel/pulls/307 +- Astro build by @felixroos in https://codeberg.org/uzu/strudel/pulls/315 +- Reference tab sort by @felixroos in https://codeberg.org/uzu/strudel/pulls/318 +- tutorial updates by @jarmitage in https://codeberg.org/uzu/strudel/pulls/320 +- support notes without octave by @felixroos in https://codeberg.org/uzu/strudel/pulls/323 +- mini repl improvements by @felixroos in https://codeberg.org/uzu/strudel/pulls/324 +- fix: workaround Object.assign globalThis by @felixroos in https://codeberg.org/uzu/strudel/pulls/326 +- add examples route by @felixroos in https://codeberg.org/uzu/strudel/pulls/327 +- add my-patterns by @felixroos in https://codeberg.org/uzu/strudel/pulls/328 +- my-patterns build + deploy by @felixroos in https://codeberg.org/uzu/strudel/pulls/329 +- my-patterns: fix paths + update readme by @felixroos in https://codeberg.org/uzu/strudel/pulls/330 +- improve displaying 's' in pianoroll by @felixroos in https://codeberg.org/uzu/strudel/pulls/331 +- fix: can now multiply floats in mini notation by @felixroos in https://codeberg.org/uzu/strudel/pulls/332 +- Embed mode improvements by @felixroos in https://codeberg.org/uzu/strudel/pulls/333 +- testing + docs docs by @felixroos in https://codeberg.org/uzu/strudel/pulls/334 +- animate mvp by @felixroos in https://codeberg.org/uzu/strudel/pulls/335 +- Tidy parser, implement polymeters by @yaxu in https://codeberg.org/uzu/strudel/pulls/336 +- animation options by @felixroos in https://codeberg.org/uzu/strudel/pulls/337 +- move /my-patterns to /swatch by @yaxu in https://codeberg.org/uzu/strudel/pulls/338 +- more animate functions + mini repl fix by @felixroos in https://codeberg.org/uzu/strudel/pulls/340 +- Patternify euclid, fast, slow and polymeter step parameters in mininotation by @yaxu in https://codeberg.org/uzu/strudel/pulls/341 +- fixes #346 by @felixroos in https://codeberg.org/uzu/strudel/pulls/347 +- Fix prebake base path by @felixroos in https://codeberg.org/uzu/strudel/pulls/345 +- Fix Bjorklund by @yaxu in https://codeberg.org/uzu/strudel/pulls/343 +- docs: tidal comparison + add global fx + add missing sampler fx by @felixroos in https://codeberg.org/uzu/strudel/pulls/356 +- Fix .out(), renaming webaudio's out() to webaudio() by @yaxu in https://codeberg.org/uzu/strudel/pulls/361 +- Support for multiple mininotation operators by @yaxu in https://codeberg.org/uzu/strudel/pulls/350 +- doc structuring by @felixroos in https://codeberg.org/uzu/strudel/pulls/360 +- add https to url by @urswilke in https://codeberg.org/uzu/strudel/pulls/364 +- document more functions + change arp join by @felixroos in https://codeberg.org/uzu/strudel/pulls/369 +- improve new draw logic by @felixroos in https://codeberg.org/uzu/strudel/pulls/372 +- Draw fixes by @felixroos in https://codeberg.org/uzu/strudel/pulls/377 +- update my-patterns instructions by @felixroos in https://codeberg.org/uzu/strudel/pulls/384 +- docs: use note instead of n to mitigate confusion by @felixroos in https://codeberg.org/uzu/strudel/pulls/385 +- add run + test + docs by @felixroos in https://codeberg.org/uzu/strudel/pulls/386 +- Rename a to angle by @felixroos in https://codeberg.org/uzu/strudel/pulls/387 +- document csound by @felixroos in https://codeberg.org/uzu/strudel/pulls/391 +- Notes are not essential :) by @yaxu in https://codeberg.org/uzu/strudel/pulls/393 +- add ribbon + test + docs by @felixroos in https://codeberg.org/uzu/strudel/pulls/388 +- Add tidal-drum-patterns to examples by @urswilke in https://codeberg.org/uzu/strudel/pulls/379 +- add pattern methods hurry, press and pressBy by @yaxu in https://codeberg.org/uzu/strudel/pulls/397 +- proper builds + use pnpm workspaces by @felixroos in https://codeberg.org/uzu/strudel/pulls/396 +- fix: minirepl styles by @felixroos in https://codeberg.org/uzu/strudel/pulls/398 +- can now await initAudio + initAudioOnFirstClick by @felixroos in https://codeberg.org/uzu/strudel/pulls/399 +- release webaudio by @felixroos in https://codeberg.org/uzu/strudel/pulls/400 ## New Contributors -- @jarmitage made their first contribution in https://github.com/tidalcycles/strudel/pull/320 -- @urswilke made their first contribution in https://github.com/tidalcycles/strudel/pull/364 +- @jarmitage made their first contribution in https://codeberg.org/uzu/strudel/pulls/320 +- @urswilke made their first contribution in https://codeberg.org/uzu/strudel/pulls/364 -**Full Changelog**: https://github.com/tidalcycles/strudel/compare/v0.5.0...v0.6.0 +**Full Changelog**: https://codeberg.org/uzu/strudel/compare/v0.5.0...v0.6.0 diff --git a/website/src/content/blog/release-0.7.0-zuckerguss.mdx b/website/src/content/blog/release-0.7.0-zuckerguss.mdx index 0a3a4649b..f67deee73 100644 --- a/website/src/content/blog/release-0.7.0-zuckerguss.mdx +++ b/website/src/content/blog/release-0.7.0-zuckerguss.mdx @@ -23,66 +23,66 @@ author: froos ## What's Changed -- pin @csound/browser to 6.18.3 + bump by @felixroos in https://github.com/tidalcycles/strudel/pull/403 -- update csound + fix sound output by @felixroos in https://github.com/tidalcycles/strudel/pull/404 -- fix: share url on subpath by @felixroos in https://github.com/tidalcycles/strudel/pull/405 -- add shabda doc by @felixroos in https://github.com/tidalcycles/strudel/pull/407 -- Update effects.mdx by @bwagner in https://github.com/tidalcycles/strudel/pull/410 -- improve effects doc by @felixroos in https://github.com/tidalcycles/strudel/pull/409 -- google gtfo by @felixroos in https://github.com/tidalcycles/strudel/pull/413 -- improve samples doc by @felixroos in https://github.com/tidalcycles/strudel/pull/411 -- PWA with offline support by @felixroos in https://github.com/tidalcycles/strudel/pull/417 -- add caching strategy for missing file types + cache all samples loaded from github by @felixroos in https://github.com/tidalcycles/strudel/pull/419 -- add more offline caching by @felixroos in https://github.com/tidalcycles/strudel/pull/421 -- add cdn.freesound to cache list by @felixroos in https://github.com/tidalcycles/strudel/pull/425 -- minirepl: add keyboard shortcuts by @felixroos in https://github.com/tidalcycles/strudel/pull/429 -- Themes by @felixroos in https://github.com/tidalcycles/strudel/pull/431 -- autocomplete preparations by @felixroos in https://github.com/tidalcycles/strudel/pull/427 -- Fix anchors by @felixroos in https://github.com/tidalcycles/strudel/pull/433 -- Update code.mdx by @bwagner in https://github.com/tidalcycles/strudel/pull/436 -- Update mini-notation.mdx by @bwagner in https://github.com/tidalcycles/strudel/pull/437 -- Update synths.mdx by @bwagner in https://github.com/tidalcycles/strudel/pull/438 -- FIXES: Warning about jsxBracketSameLine deprecation by @bwagner in https://github.com/tidalcycles/strudel/pull/461 -- Composable functions by @yaxu in https://github.com/tidalcycles/strudel/pull/390 -- weave and weaveWith by @yaxu in https://github.com/tidalcycles/strudel/pull/465 -- slice and splice by @yaxu in https://github.com/tidalcycles/strudel/pull/466 -- fix: osc should not return a promise by @felixroos in https://github.com/tidalcycles/strudel/pull/472 -- FIXES: freqs instead of pitches by @bwagner in https://github.com/tidalcycles/strudel/pull/464 -- Update input-output.mdx by @bwagner in https://github.com/tidalcycles/strudel/pull/471 -- settings tab with vim / emacs modes + additional themes and fonts by @felixroos in https://github.com/tidalcycles/strudel/pull/467 -- fix: hash links by @felixroos in https://github.com/tidalcycles/strudel/pull/473 -- midi cc support by @felixroos in https://github.com/tidalcycles/strudel/pull/478 -- Fix array args by @felixroos in https://github.com/tidalcycles/strudel/pull/480 -- docs: packages + offline by @felixroos in https://github.com/tidalcycles/strudel/pull/482 -- Update mini-notation.mdx by @yaxu in https://github.com/tidalcycles/strudel/pull/365 -- Revert "Another attempt at composable functions - WIP (#390)" by @felixroos in https://github.com/tidalcycles/strudel/pull/484 -- fix app height by @felixroos in https://github.com/tidalcycles/strudel/pull/485 -- add algolia creds + optimize sidebar for crawling by @felixroos in https://github.com/tidalcycles/strudel/pull/488 -- refactor react package by @felixroos in https://github.com/tidalcycles/strudel/pull/490 -- react style fixes by @felixroos in https://github.com/tidalcycles/strudel/pull/491 -- implement cps in scheduler by @felixroos in https://github.com/tidalcycles/strudel/pull/493 -- Add control aliases by @yaxu in https://github.com/tidalcycles/strudel/pull/497 -- fix: nano-repl highlighting by @felixroos in https://github.com/tidalcycles/strudel/pull/501 -- Reinstate slice and splice by @yaxu in https://github.com/tidalcycles/strudel/pull/500 -- can now use : as a replacement for space in scales by @felixroos in https://github.com/tidalcycles/strudel/pull/502 -- Support list syntax in mininotation by @yaxu in https://github.com/tidalcycles/strudel/pull/512 -- update react to 18 by @felixroos in https://github.com/tidalcycles/strudel/pull/514 -- add arrange function by @felixroos in https://github.com/tidalcycles/strudel/pull/508 -- Update README.md by @bwagner in https://github.com/tidalcycles/strudel/pull/474 -- add 2 illegible fonts by @felixroos in https://github.com/tidalcycles/strudel/pull/518 -- registerSound API + improved sounds tab + regroup soundfonts by @felixroos in https://github.com/tidalcycles/strudel/pull/516 -- fix: envelopes in chrome by @felixroos in https://github.com/tidalcycles/strudel/pull/521 -- Update samples.mdx by @bwagner in https://github.com/tidalcycles/strudel/pull/524 -- Update intro.mdx by @bwagner in https://github.com/tidalcycles/strudel/pull/525 -- fix(footer): fix link to tidalcycles by @revolunet in https://github.com/tidalcycles/strudel/pull/529 -- FIXES: alias pm for polymeter by @bwagner in https://github.com/tidalcycles/strudel/pull/527 -- Maintain random seed state in parser, not globally by @ijc8 in https://github.com/tidalcycles/strudel/pull/531 -- feat: add freq support to gm soundfonts by @felixroos in https://github.com/tidalcycles/strudel/pull/534 -- Update lerna by @felixroos in https://github.com/tidalcycles/strudel/pull/535 +- pin @csound/browser to 6.18.3 + bump by @felixroos in https://codeberg.org/uzu/strudel/pulls/403 +- update csound + fix sound output by @felixroos in https://codeberg.org/uzu/strudel/pulls/404 +- fix: share url on subpath by @felixroos in https://codeberg.org/uzu/strudel/pulls/405 +- add shabda doc by @felixroos in https://codeberg.org/uzu/strudel/pulls/407 +- Update effects.mdx by @bwagner in https://codeberg.org/uzu/strudel/pulls/410 +- improve effects doc by @felixroos in https://codeberg.org/uzu/strudel/pulls/409 +- google gtfo by @felixroos in https://codeberg.org/uzu/strudel/pulls/413 +- improve samples doc by @felixroos in https://codeberg.org/uzu/strudel/pulls/411 +- PWA with offline support by @felixroos in https://codeberg.org/uzu/strudel/pulls/417 +- add caching strategy for missing file types + cache all samples loaded from github by @felixroos in https://codeberg.org/uzu/strudel/pulls/419 +- add more offline caching by @felixroos in https://codeberg.org/uzu/strudel/pulls/421 +- add cdn.freesound to cache list by @felixroos in https://codeberg.org/uzu/strudel/pulls/425 +- minirepl: add keyboard shortcuts by @felixroos in https://codeberg.org/uzu/strudel/pulls/429 +- Themes by @felixroos in https://codeberg.org/uzu/strudel/pulls/431 +- autocomplete preparations by @felixroos in https://codeberg.org/uzu/strudel/pulls/427 +- Fix anchors by @felixroos in https://codeberg.org/uzu/strudel/pulls/433 +- Update code.mdx by @bwagner in https://codeberg.org/uzu/strudel/pulls/436 +- Update mini-notation.mdx by @bwagner in https://codeberg.org/uzu/strudel/pulls/437 +- Update synths.mdx by @bwagner in https://codeberg.org/uzu/strudel/pulls/438 +- FIXES: Warning about jsxBracketSameLine deprecation by @bwagner in https://codeberg.org/uzu/strudel/pulls/461 +- Composable functions by @yaxu in https://codeberg.org/uzu/strudel/pulls/390 +- weave and weaveWith by @yaxu in https://codeberg.org/uzu/strudel/pulls/465 +- slice and splice by @yaxu in https://codeberg.org/uzu/strudel/pulls/466 +- fix: osc should not return a promise by @felixroos in https://codeberg.org/uzu/strudel/pulls/472 +- FIXES: freqs instead of pitches by @bwagner in https://codeberg.org/uzu/strudel/pulls/464 +- Update input-output.mdx by @bwagner in https://codeberg.org/uzu/strudel/pulls/471 +- settings tab with vim / emacs modes + additional themes and fonts by @felixroos in https://codeberg.org/uzu/strudel/pulls/467 +- fix: hash links by @felixroos in https://codeberg.org/uzu/strudel/pulls/473 +- midi cc support by @felixroos in https://codeberg.org/uzu/strudel/pulls/478 +- Fix array args by @felixroos in https://codeberg.org/uzu/strudel/pulls/480 +- docs: packages + offline by @felixroos in https://codeberg.org/uzu/strudel/pulls/482 +- Update mini-notation.mdx by @yaxu in https://codeberg.org/uzu/strudel/pulls/365 +- Revert "Another attempt at composable functions - WIP (#390)" by @felixroos in https://codeberg.org/uzu/strudel/pulls/484 +- fix app height by @felixroos in https://codeberg.org/uzu/strudel/pulls/485 +- add algolia creds + optimize sidebar for crawling by @felixroos in https://codeberg.org/uzu/strudel/pulls/488 +- refactor react package by @felixroos in https://codeberg.org/uzu/strudel/pulls/490 +- react style fixes by @felixroos in https://codeberg.org/uzu/strudel/pulls/491 +- implement cps in scheduler by @felixroos in https://codeberg.org/uzu/strudel/pulls/493 +- Add control aliases by @yaxu in https://codeberg.org/uzu/strudel/pulls/497 +- fix: nano-repl highlighting by @felixroos in https://codeberg.org/uzu/strudel/pulls/501 +- Reinstate slice and splice by @yaxu in https://codeberg.org/uzu/strudel/pulls/500 +- can now use : as a replacement for space in scales by @felixroos in https://codeberg.org/uzu/strudel/pulls/502 +- Support list syntax in mininotation by @yaxu in https://codeberg.org/uzu/strudel/pulls/512 +- update react to 18 by @felixroos in https://codeberg.org/uzu/strudel/pulls/514 +- add arrange function by @felixroos in https://codeberg.org/uzu/strudel/pulls/508 +- Update README.md by @bwagner in https://codeberg.org/uzu/strudel/pulls/474 +- add 2 illegible fonts by @felixroos in https://codeberg.org/uzu/strudel/pulls/518 +- registerSound API + improved sounds tab + regroup soundfonts by @felixroos in https://codeberg.org/uzu/strudel/pulls/516 +- fix: envelopes in chrome by @felixroos in https://codeberg.org/uzu/strudel/pulls/521 +- Update samples.mdx by @bwagner in https://codeberg.org/uzu/strudel/pulls/524 +- Update intro.mdx by @bwagner in https://codeberg.org/uzu/strudel/pulls/525 +- fix(footer): fix link to tidalcycles by @revolunet in https://codeberg.org/uzu/strudel/pulls/529 +- FIXES: alias pm for polymeter by @bwagner in https://codeberg.org/uzu/strudel/pulls/527 +- Maintain random seed state in parser, not globally by @ijc8 in https://codeberg.org/uzu/strudel/pulls/531 +- feat: add freq support to gm soundfonts by @felixroos in https://codeberg.org/uzu/strudel/pulls/534 +- Update lerna by @felixroos in https://codeberg.org/uzu/strudel/pulls/535 ## New Contributors -- @revolunet made their first contribution in https://github.com/tidalcycles/strudel/pull/529 -- @ijc8 made their first contribution in https://github.com/tidalcycles/strudel/pull/531 +- @revolunet made their first contribution in https://codeberg.org/uzu/strudel/pulls/529 +- @ijc8 made their first contribution in https://codeberg.org/uzu/strudel/pulls/531 -**Full Changelog**: https://github.com/tidalcycles/strudel/compare/v0.6.0...v0.7.0 +**Full Changelog**: https://codeberg.org/uzu/strudel/compare/v0.6.0...v0.7.0 diff --git a/website/src/content/blog/release-0.8.0-himbeermuffin.mdx b/website/src/content/blog/release-0.8.0-himbeermuffin.mdx index 7620140b5..babab5954 100644 --- a/website/src/content/blog/release-0.8.0-himbeermuffin.mdx +++ b/website/src/content/blog/release-0.8.0-himbeermuffin.mdx @@ -82,49 +82,49 @@ A big thanks to all the contributors! ## What's Changed -- fix period key for dvorak + remove duplicated code by @felixroos in https://github.com/tidalcycles/strudel/pull/537 -- improve initial loading + wait before eval by @felixroos in https://github.com/tidalcycles/strudel/pull/538 -- do not reset cps before eval #517 by @felixroos in https://github.com/tidalcycles/strudel/pull/539 -- feat: add loader bar to animate loading state by @felixroos in https://github.com/tidalcycles/strudel/pull/542 -- add firacode font by @felixroos in https://github.com/tidalcycles/strudel/pull/544 -- fix: allow whitespace at the end of a mini pattern by @felixroos in https://github.com/tidalcycles/strudel/pull/547 -- fix: reset time on stop by @felixroos in https://github.com/tidalcycles/strudel/pull/548 -- fix: load soundfonts in prebake by @felixroos in https://github.com/tidalcycles/strudel/pull/550 -- fix: colorable highlighting by @felixroos in https://github.com/tidalcycles/strudel/pull/553 -- fix: make soundfonts import dynamic by @felixroos in https://github.com/tidalcycles/strudel/pull/556 -- add basic triads and guidetone voicings by @felixroos in https://github.com/tidalcycles/strudel/pull/557 -- Patchday by @felixroos in https://github.com/tidalcycles/strudel/pull/559 -- Vanilla JS Refactoring by @felixroos in https://github.com/tidalcycles/strudel/pull/563 -- repl: add option to display line numbers by @roipoussiere in https://github.com/tidalcycles/strudel/pull/582 -- learn/tonal: fix typo in "scaleTran[s]pose" by @srenatus in https://github.com/tidalcycles/strudel/pull/585 -- Music metadata by @roipoussiere in https://github.com/tidalcycles/strudel/pull/580 -- New Workshop by @felixroos in https://github.com/tidalcycles/strudel/pull/587 -- Fix option dot by @felixroos in https://github.com/tidalcycles/strudel/pull/596 -- fix: allow f for flat notes like tidal by @felixroos in https://github.com/tidalcycles/strudel/pull/593 -- fix: division by zero by @felixroos in https://github.com/tidalcycles/strudel/pull/591 -- Solmization added by @dariacotocu in https://github.com/tidalcycles/strudel/pull/570 -- improve cursor by @felixroos in https://github.com/tidalcycles/strudel/pull/597 -- enable auto-completion by @roipoussiere in https://github.com/tidalcycles/strudel/pull/588 -- add ratio function by @felixroos in https://github.com/tidalcycles/strudel/pull/602 -- editor: enable line wrapping by @roipoussiere in https://github.com/tidalcycles/strudel/pull/581 -- tonal fixes by @felixroos in https://github.com/tidalcycles/strudel/pull/607 -- fix: flatten scale lists by @felixroos in https://github.com/tidalcycles/strudel/pull/605 -- clip now works like legato in tidal by @felixroos in https://github.com/tidalcycles/strudel/pull/598 -- fix: doc links by @felixroos in https://github.com/tidalcycles/strudel/pull/612 -- tauri desktop app by @vasilymilovidov in https://github.com/tidalcycles/strudel/pull/613 -- add spiral viz by @felixroos in https://github.com/tidalcycles/strudel/pull/614 -- patterning ui settings by @felixroos in https://github.com/tidalcycles/strudel/pull/606 -- Fix typo on packages.mdx by @paikwiki in https://github.com/tidalcycles/strudel/pull/520 -- cps dependent functions by @felixroos in https://github.com/tidalcycles/strudel/pull/620 -- desktop: play samples from disk by @felixroos in https://github.com/tidalcycles/strudel/pull/621 -- fix: midi clock drift by @felixroos in https://github.com/tidalcycles/strudel/pull/627 +- fix period key for dvorak + remove duplicated code by @felixroos in https://codeberg.org/uzu/strudel/pulls/537 +- improve initial loading + wait before eval by @felixroos in https://codeberg.org/uzu/strudel/pulls/538 +- do not reset cps before eval #517 by @felixroos in https://codeberg.org/uzu/strudel/pulls/539 +- feat: add loader bar to animate loading state by @felixroos in https://codeberg.org/uzu/strudel/pulls/542 +- add firacode font by @felixroos in https://codeberg.org/uzu/strudel/pulls/544 +- fix: allow whitespace at the end of a mini pattern by @felixroos in https://codeberg.org/uzu/strudel/pulls/547 +- fix: reset time on stop by @felixroos in https://codeberg.org/uzu/strudel/pulls/548 +- fix: load soundfonts in prebake by @felixroos in https://codeberg.org/uzu/strudel/pulls/550 +- fix: colorable highlighting by @felixroos in https://codeberg.org/uzu/strudel/pulls/553 +- fix: make soundfonts import dynamic by @felixroos in https://codeberg.org/uzu/strudel/pulls/556 +- add basic triads and guidetone voicings by @felixroos in https://codeberg.org/uzu/strudel/pulls/557 +- Patchday by @felixroos in https://codeberg.org/uzu/strudel/pulls/559 +- Vanilla JS Refactoring by @felixroos in https://codeberg.org/uzu/strudel/pulls/563 +- repl: add option to display line numbers by @roipoussiere in https://codeberg.org/uzu/strudel/pulls/582 +- learn/tonal: fix typo in "scaleTran[s]pose" by @srenatus in https://codeberg.org/uzu/strudel/pulls/585 +- Music metadata by @roipoussiere in https://codeberg.org/uzu/strudel/pulls/580 +- New Workshop by @felixroos in https://codeberg.org/uzu/strudel/pulls/587 +- Fix option dot by @felixroos in https://codeberg.org/uzu/strudel/pulls/596 +- fix: allow f for flat notes like tidal by @felixroos in https://codeberg.org/uzu/strudel/pulls/593 +- fix: division by zero by @felixroos in https://codeberg.org/uzu/strudel/pulls/591 +- Solmization added by @dariacotocu in https://codeberg.org/uzu/strudel/pulls/570 +- improve cursor by @felixroos in https://codeberg.org/uzu/strudel/pulls/597 +- enable auto-completion by @roipoussiere in https://codeberg.org/uzu/strudel/pulls/588 +- add ratio function by @felixroos in https://codeberg.org/uzu/strudel/pulls/602 +- editor: enable line wrapping by @roipoussiere in https://codeberg.org/uzu/strudel/pulls/581 +- tonal fixes by @felixroos in https://codeberg.org/uzu/strudel/pulls/607 +- fix: flatten scale lists by @felixroos in https://codeberg.org/uzu/strudel/pulls/605 +- clip now works like legato in tidal by @felixroos in https://codeberg.org/uzu/strudel/pulls/598 +- fix: doc links by @felixroos in https://codeberg.org/uzu/strudel/pulls/612 +- tauri desktop app by @vasilymilovidov in https://codeberg.org/uzu/strudel/pulls/613 +- add spiral viz by @felixroos in https://codeberg.org/uzu/strudel/pulls/614 +- patterning ui settings by @felixroos in https://codeberg.org/uzu/strudel/pulls/606 +- Fix typo on packages.mdx by @paikwiki in https://codeberg.org/uzu/strudel/pulls/520 +- cps dependent functions by @felixroos in https://codeberg.org/uzu/strudel/pulls/620 +- desktop: play samples from disk by @felixroos in https://codeberg.org/uzu/strudel/pulls/621 +- fix: midi clock drift by @felixroos in https://codeberg.org/uzu/strudel/pulls/627 ## New Contributors -- @roipoussiere made their first contribution in https://github.com/tidalcycles/strudel/pull/582 -- @srenatus made their first contribution in https://github.com/tidalcycles/strudel/pull/585 -- @dariacotocu made their first contribution in https://github.com/tidalcycles/strudel/pull/570 -- @vasilymilovidov made their first contribution in https://github.com/tidalcycles/strudel/pull/613 -- @paikwiki made their first contribution in https://github.com/tidalcycles/strudel/pull/520 +- @roipoussiere made their first contribution in https://codeberg.org/uzu/strudel/pulls/582 +- @srenatus made their first contribution in https://codeberg.org/uzu/strudel/pulls/585 +- @dariacotocu made their first contribution in https://codeberg.org/uzu/strudel/pulls/570 +- @vasilymilovidov made their first contribution in https://codeberg.org/uzu/strudel/pulls/613 +- @paikwiki made their first contribution in https://codeberg.org/uzu/strudel/pulls/520 -**Full Changelog**: https://github.com/tidalcycles/strudel/compare/v0.7.0...v0.8.0 +**Full Changelog**: https://codeberg.org/uzu/strudel/compare/v0.7.0...v0.8.0 diff --git a/website/src/content/blog/release-0.9.0-bananenbrot.mdx b/website/src/content/blog/release-0.9.0-bananenbrot.mdx index ae77b4247..944675e38 100644 --- a/website/src/content/blog/release-0.9.0-bananenbrot.mdx +++ b/website/src/content/blog/release-0.9.0-bananenbrot.mdx @@ -29,14 +29,14 @@ Main new features include: Related PRs: -- superdough: encapsulates web audio output by @felixroos in https://github.com/tidalcycles/strudel/pull/664 -- basic fm by @felixroos in https://github.com/tidalcycles/strudel/pull/669 -- Wave Selection and Global Envelope on the FM Synth Modulator by @Bubobubobubobubo in https://github.com/tidalcycles/strudel/pull/683 -- control osc partial count with n by @felixroos in https://github.com/tidalcycles/strudel/pull/674 -- ZZFX Synth support by @Bubobubobubobubo in https://github.com/tidalcycles/strudel/pull/684 -- Adding filter envelopes and filter order selection by @Bubobubobubobubo in https://github.com/tidalcycles/strudel/pull/692 -- Adding loop points and thus wavetable synthesis by @Bubobubobubobubo in https://github.com/tidalcycles/strudel/pull/698 -- Adding vibrato to base oscillators by @Bubobubobubobubo in https://github.com/tidalcycles/strudel/pull/693 +- superdough: encapsulates web audio output by @felixroos in https://codeberg.org/uzu/strudel/pulls/664 +- basic fm by @felixroos in https://codeberg.org/uzu/strudel/pulls/669 +- Wave Selection and Global Envelope on the FM Synth Modulator by @Bubobubobubobubo in https://codeberg.org/uzu/strudel/pulls/683 +- control osc partial count with n by @felixroos in https://codeberg.org/uzu/strudel/pulls/674 +- ZZFX Synth support by @Bubobubobubobubo in https://codeberg.org/uzu/strudel/pulls/684 +- Adding filter envelopes and filter order selection by @Bubobubobubobubo in https://codeberg.org/uzu/strudel/pulls/692 +- Adding loop points and thus wavetable synthesis by @Bubobubobubobubo in https://codeberg.org/uzu/strudel/pulls/698 +- Adding vibrato to base oscillators by @Bubobubobubobubo in https://codeberg.org/uzu/strudel/pulls/693 ## Desktop App Improvements @@ -45,70 +45,70 @@ which do not depend on browser APIs! You can see superdough, superdirt via OSC + hardware synths via MIDI all together playing in harmony in this [awesome video](https://www.youtube.com/watch?v=lxQgBeLQBgk). These are the related PRs: -- Create Midi Integration for Tauri Desktop app by @daslyfe in https://github.com/tidalcycles/strudel/pull/685 -- add sleep timer + improve message iterating by @daslyfe in https://github.com/tidalcycles/strudel/pull/688 -- fix MIDI CC messages by @vasilymilovidov in https://github.com/tidalcycles/strudel/pull/690 -- Direct OSC Support in Tauri by @daslyfe in https://github.com/tidalcycles/strudel/pull/694 -- Add logging from tauri by @daslyfe in https://github.com/tidalcycles/strudel/pull/697 -- fix osc bundle timestamp glitches caused by drifting clock by @daslyfe in https://github.com/tidalcycles/strudel/pull/666 -- Midi time fixes by @daslyfe in https://github.com/tidalcycles/strudel/pull/668 -- [Bug Fix] Account for numeral notation when converting to midi by @daslyfe in https://github.com/tidalcycles/strudel/pull/656 -- [Bug Fix] Midi: Don't treat note 0 as false by @daslyfe in https://github.com/tidalcycles/strudel/pull/657 +- Create Midi Integration for Tauri Desktop app by @daslyfe in https://codeberg.org/uzu/strudel/pulls/685 +- add sleep timer + improve message iterating by @daslyfe in https://codeberg.org/uzu/strudel/pulls/688 +- fix MIDI CC messages by @vasilymilovidov in https://codeberg.org/uzu/strudel/pulls/690 +- Direct OSC Support in Tauri by @daslyfe in https://codeberg.org/uzu/strudel/pulls/694 +- Add logging from tauri by @daslyfe in https://codeberg.org/uzu/strudel/pulls/697 +- fix osc bundle timestamp glitches caused by drifting clock by @daslyfe in https://codeberg.org/uzu/strudel/pulls/666 +- Midi time fixes by @daslyfe in https://codeberg.org/uzu/strudel/pulls/668 +- [Bug Fix] Account for numeral notation when converting to midi by @daslyfe in https://codeberg.org/uzu/strudel/pulls/656 +- [Bug Fix] Midi: Don't treat note 0 as false by @daslyfe in https://codeberg.org/uzu/strudel/pulls/657 ## Visuals -- 2 new FFT based vizualisations have now landed: [scope and fscope](https://github.com/tidalcycles/strudel/pull/677) (featured in the video at the top). -- pianoroll has new options, see [PR](https://github.com/tidalcycles/strudel/pull/679) +- 2 new FFT based vizualisations have now landed: [scope and fscope](https://codeberg.org/uzu/strudel/pulls/677) (featured in the video at the top). +- pianoroll has new options, see [PR](https://codeberg.org/uzu/strudel/pulls/679) Related PRs: -- Scope by @felixroos in https://github.com/tidalcycles/strudel/pull/677 ([demo](https://strudel.tidalcycles.org/?hXVQF-KxMI8p)) -- Pianoroll improvements by @felixroos in https://github.com/tidalcycles/strudel/pull/679 ([demo](https://strudel.tidalcycles.org/?aPMKqXGVMgSM)) +- Scope by @felixroos in https://codeberg.org/uzu/strudel/pulls/677 ([demo](https://strudel.tidalcycles.org/?hXVQF-KxMI8p)) +- Pianoroll improvements by @felixroos in https://codeberg.org/uzu/strudel/pulls/679 ([demo](https://strudel.tidalcycles.org/?aPMKqXGVMgSM)) ## Voicings There is now a new way to play chord voicings + a huge selection of chord voicings available. Find out more in these PRs: -- stateless voicings + tonleiter lib by @felixroos in https://github.com/tidalcycles/strudel/pull/647 ([demo](https://strudel.tidalcycles.org/?FoILM0Hs9y9f)) -- ireal voicings by @felixroos in https://github.com/tidalcycles/strudel/pull/653 ([demo](https://strudel.tidalcycles.org/?bv_TjY9hOC28)) +- stateless voicings + tonleiter lib by @felixroos in https://codeberg.org/uzu/strudel/pulls/647 ([demo](https://strudel.tidalcycles.org/?FoILM0Hs9y9f)) +- ireal voicings by @felixroos in https://codeberg.org/uzu/strudel/pulls/653 ([demo](https://strudel.tidalcycles.org/?bv_TjY9hOC28)) ## Adaptive Highlighting Thanks to @mindofmatthew , the highlighting will adapt to edits instantly! Related PRs: -- More work on highlight IDs by @mindofmatthew in https://github.com/tidalcycles/strudel/pull/636 -- Adaptive Highlighting by @felixroos in https://github.com/tidalcycles/strudel/pull/634 +- More work on highlight IDs by @mindofmatthew in https://codeberg.org/uzu/strudel/pulls/636 +- Adaptive Highlighting by @felixroos in https://codeberg.org/uzu/strudel/pulls/634 ## UI Changes -- teletext theme + fonts by @felixroos in https://github.com/tidalcycles/strudel/pull/681 (featured in video at the top) -- togglable panel position by @felixroos in https://github.com/tidalcycles/strudel/pull/667 +- teletext theme + fonts by @felixroos in https://codeberg.org/uzu/strudel/pulls/681 (featured in video at the top) +- togglable panel position by @felixroos in https://codeberg.org/uzu/strudel/pulls/667 ## Other New Features -- slice: list mode by @felixroos in https://github.com/tidalcycles/strudel/pull/645 ([demo](https://strudel.tidalcycles.org/?bAYIqz5NLjRr)) -- add emoji support by @felixroos in https://github.com/tidalcycles/strudel/pull/680 ([demo](https://strudel.tidalcycles.org/?a6FgLz475gN9)) +- slice: list mode by @felixroos in https://codeberg.org/uzu/strudel/pulls/645 ([demo](https://strudel.tidalcycles.org/?bAYIqz5NLjRr)) +- add emoji support by @felixroos in https://codeberg.org/uzu/strudel/pulls/680 ([demo](https://strudel.tidalcycles.org/?a6FgLz475gN9)) ## Articles -- Understand pitch by @felixroos in https://github.com/tidalcycles/strudel/pull/652 +- Understand pitch by @felixroos in https://codeberg.org/uzu/strudel/pulls/652 ## Other Fixes & Enhancements -- fix: out of range error by @felixroos in https://github.com/tidalcycles/strudel/pull/630 -- fix: update canvas size on window resize by @felixroos in https://github.com/tidalcycles/strudel/pull/631 -- FIXES: TODO in rotateChroma by @bwagner in https://github.com/tidalcycles/strudel/pull/650 -- snapshot tests: sort haps by part by @felixroos in https://github.com/tidalcycles/strudel/pull/637 -- Delete old packages by @felixroos in https://github.com/tidalcycles/strudel/pull/639 -- update vitest by @felixroos in https://github.com/tidalcycles/strudel/pull/651 -- fix: welcome message for latestCode by @felixroos in https://github.com/tidalcycles/strudel/pull/659 -- fix: always run previous trigger by @felixroos in https://github.com/tidalcycles/strudel/pull/660 +- fix: out of range error by @felixroos in https://codeberg.org/uzu/strudel/pulls/630 +- fix: update canvas size on window resize by @felixroos in https://codeberg.org/uzu/strudel/pulls/631 +- FIXES: TODO in rotateChroma by @bwagner in https://codeberg.org/uzu/strudel/pulls/650 +- snapshot tests: sort haps by part by @felixroos in https://codeberg.org/uzu/strudel/pulls/637 +- Delete old packages by @felixroos in https://codeberg.org/uzu/strudel/pulls/639 +- update vitest by @felixroos in https://codeberg.org/uzu/strudel/pulls/651 +- fix: welcome message for latestCode by @felixroos in https://codeberg.org/uzu/strudel/pulls/659 +- fix: always run previous trigger by @felixroos in https://codeberg.org/uzu/strudel/pulls/660 ## New Contributors -- @daslyfe made their first contribution in https://github.com/tidalcycles/strudel/pull/656 -- @Bubobubobubobubo made their first contribution in https://github.com/tidalcycles/strudel/pull/683 +- @daslyfe made their first contribution in https://codeberg.org/uzu/strudel/pulls/656 +- @Bubobubobubobubo made their first contribution in https://codeberg.org/uzu/strudel/pulls/683 -**Full Changelog**: https://github.com/tidalcycles/strudel/compare/v0.8.0...v0.9.0 +**Full Changelog**: https://codeberg.org/uzu/strudel/compare/v0.8.0...v0.9.0 A big thanks to all the contributors! diff --git a/website/src/content/blog/release-1.0.0-geburtstagskuchen.mdx b/website/src/content/blog/release-1.0.0-geburtstagskuchen.mdx index 5784fe52d..0ee149a20 100644 --- a/website/src/content/blog/release-1.0.0-geburtstagskuchen.mdx +++ b/website/src/content/blog/release-1.0.0-geburtstagskuchen.mdx @@ -27,13 +27,13 @@ Let me write up some of the highlights: This version changes the default cps value from 1 to 0.5 to give patterns a little bit more time by default. If you find your existing patterns to be suddenly half the speed, just add a `setcps(1)` to the top and it should sound as it did before! -- make 0.5hz cps the default by @yaxu in https://github.com/tidalcycles/strudel/pull/931 +- make 0.5hz cps the default by @yaxu in https://codeberg.org/uzu/strudel/pulls/931 ## New Domain Strudel is now available under [strudel.cc](https://strudel.cc/). The old domain still works but you might not get the most recent version. -- replace strudel.tidalcycles.org with strudel.cc by @felixroos in https://github.com/tidalcycles/strudel/pull/768 +- replace strudel.tidalcycles.org with strudel.cc by @felixroos in https://codeberg.org/uzu/strudel/pulls/768 ## Strudel on Mastodon @@ -43,52 +43,52 @@ Strudel now has a mastodon presence: https://social.toplap.org/@strudel superdough, the audio engine of strudel has gotten some new features: -- Create phaser effect by @daslyfe in https://github.com/tidalcycles/strudel/pull/798 -- Multichannel audio by @daslyfe in https://github.com/tidalcycles/strudel/pull/820 -- Audio device selection by @daslyfe in https://github.com/tidalcycles/strudel/pull/854 -- Better convolution reverb by generating impulse responses by @Bubobubobubobubo and @felixroos in https://github.com/tidalcycles/strudel/pull/718 -- Add 'white', 'pink' and 'brown' oscillators + refactor synth by @Bubobubobubobubo and @felixroos in https://github.com/tidalcycles/strudel/pull/713 -- New noise type: "crackle" by @Bubobubobubobubo in https://github.com/tidalcycles/strudel/pull/806 -- Add support for using samples as impulse response buffers for the reverb by @vasilymilovidov in https://github.com/tidalcycles/strudel/pull/717 -- Compressor by @felixroos in https://github.com/tidalcycles/strudel/pull/729 -- Adding vibrato to Superdough sampler by @Bubobubobubobubo and @felixroos in https://github.com/tidalcycles/strudel/pull/706 -- Further Envelope improvements by @daslyfe in https://github.com/tidalcycles/strudel/pull/868 -- Add more vowel qualities for the vowels function by @fnordomat in https://github.com/tidalcycles/strudel/pull/907 -- pitch envelope by @felixroos in https://github.com/tidalcycles/strudel/pull/913 +- Create phaser effect by @daslyfe in https://codeberg.org/uzu/strudel/pulls/798 +- Multichannel audio by @daslyfe in https://codeberg.org/uzu/strudel/pulls/820 +- Audio device selection by @daslyfe in https://codeberg.org/uzu/strudel/pulls/854 +- Better convolution reverb by generating impulse responses by @Bubobubobubobubo and @felixroos in https://codeberg.org/uzu/strudel/pulls/718 +- Add 'white', 'pink' and 'brown' oscillators + refactor synth by @Bubobubobubobubo and @felixroos in https://codeberg.org/uzu/strudel/pulls/713 +- New noise type: "crackle" by @Bubobubobubobubo in https://codeberg.org/uzu/strudel/pulls/806 +- Add support for using samples as impulse response buffers for the reverb by @vasilymilovidov in https://codeberg.org/uzu/strudel/pulls/717 +- Compressor by @felixroos in https://codeberg.org/uzu/strudel/pulls/729 +- Adding vibrato to Superdough sampler by @Bubobubobubobubo and @felixroos in https://codeberg.org/uzu/strudel/pulls/706 +- Further Envelope improvements by @daslyfe in https://codeberg.org/uzu/strudel/pulls/868 +- Add more vowel qualities for the vowels function by @fnordomat in https://codeberg.org/uzu/strudel/pulls/907 +- pitch envelope by @felixroos in https://codeberg.org/uzu/strudel/pulls/913 ## Slider Controls The new `slider` function inlines a draggable slider element into the code, bridging the gap between code and GUI. -- widgets by @felixroos in https://github.com/tidalcycles/strudel/pull/714 -- Slider afterthoughts by @felixroos in https://github.com/tidalcycles/strudel/pull/723 -- add xfade by @felixroos in https://github.com/tidalcycles/strudel/pull/780 +- widgets by @felixroos in https://codeberg.org/uzu/strudel/pulls/714 +- Slider afterthoughts by @felixroos in https://codeberg.org/uzu/strudel/pulls/723 +- add xfade by @felixroos in https://codeberg.org/uzu/strudel/pulls/780 ## Improved MIDI integration Pattern params [can now be controlled with cc messages](https://www.youtube.com/watch?v=e2-Sv_jjDQk) + you can now send a MIDI clock to sync your DAW with strudel. -- Midi in by @felixroos in https://github.com/tidalcycles/strudel/pull/699 -- add midi clock support by @felixroos in https://github.com/tidalcycles/strudel/pull/710 +- Midi in by @felixroos in https://codeberg.org/uzu/strudel/pulls/699 +- add midi clock support by @felixroos in https://codeberg.org/uzu/strudel/pulls/710 ## hydra [hydra](https://hydra.ojack.xyz), the live coding video synth [can now be used directly inside the strudel REPL](https://strudel.cc/learn/hydra/). -- Hydra integration by @felixroos in https://github.com/tidalcycles/strudel/pull/759 -- add options param to initHydra by @kasparsj in https://github.com/tidalcycles/strudel/pull/808 -- Hydra fixes and improvements by @atfornes in https://github.com/tidalcycles/strudel/pull/818 +- Hydra integration by @felixroos in https://codeberg.org/uzu/strudel/pulls/759 +- add options param to initHydra by @kasparsj in https://codeberg.org/uzu/strudel/pulls/808 +- Hydra fixes and improvements by @atfornes in https://codeberg.org/uzu/strudel/pulls/818 ## Vanilla REPL The codemirror editor and the repl abstraction have been refactored from react to vanilla JS! This should give some performance improvements and less dependency / maintenance burden: -- Vanilla repl 2 by @felixroos in https://github.com/tidalcycles/strudel/pull/863 -- Vanilla repl 3 by @felixroos in https://github.com/tidalcycles/strudel/pull/865 -- more work on vanilla repl: repl web component + package + MicroRepl by @felixroos in https://github.com/tidalcycles/strudel/pull/866 -- main repl vanillification by @felixroos in https://github.com/tidalcycles/strudel/pull/873 -- final vanillification by @felixroos in https://github.com/tidalcycles/strudel/pull/876 +- Vanilla repl 2 by @felixroos in https://codeberg.org/uzu/strudel/pulls/863 +- Vanilla repl 3 by @felixroos in https://codeberg.org/uzu/strudel/pulls/865 +- more work on vanilla repl: repl web component + package + MicroRepl by @felixroos in https://codeberg.org/uzu/strudel/pulls/866 +- main repl vanillification by @felixroos in https://codeberg.org/uzu/strudel/pulls/873 +- final vanillification by @felixroos in https://codeberg.org/uzu/strudel/pulls/876 ## Doc Changes @@ -97,25 +97,25 @@ Plenty of things have been added to the docs, including a [showcase of what peop
show PRs -- Showcase by @felixroos in https://github.com/tidalcycles/strudel/pull/885 -- Recipes by @felixroos in https://github.com/tidalcycles/strudel/pull/742 -- Document striate function by @ilesinge in https://github.com/tidalcycles/strudel/pull/766 -- Document adsr function by @ilesinge in https://github.com/tidalcycles/strudel/pull/767 -- Add function params in reference tab by @ilesinge in https://github.com/tidalcycles/strudel/pull/785 -- Update first-sounds.mdx by @bwagner in https://github.com/tidalcycles/strudel/pull/794 -- Update recap.mdx by @bwagner in https://github.com/tidalcycles/strudel/pull/797 -- Update pattern-effects.mdx by @bwagner in https://github.com/tidalcycles/strudel/pull/796 -- Update first-effects.mdx by @bwagner in https://github.com/tidalcycles/strudel/pull/795 -- Document pianoroll by @ilesinge in https://github.com/tidalcycles/strudel/pull/784 -- Add doc for euclidLegatoRot, wordfall and slider by @ilesinge in https://github.com/tidalcycles/strudel/pull/801 -- Improve documentation for synonym functions by @ilesinge in https://github.com/tidalcycles/strudel/pull/800 -- Add and style algolia search by @ilesinge in https://github.com/tidalcycles/strudel/pull/827 -- Fix a typo by @drewgbarnes in https://github.com/tidalcycles/strudel/pull/830 -- add mastodon link by @felixroos in https://github.com/tidalcycles/strudel/pull/884 -- adds a blog by @felixroos in https://github.com/tidalcycles/strudel/pull/911 -- community bakery by @felixroos in https://github.com/tidalcycles/strudel/pull/923 -- Blog improvements by @felixroos in https://github.com/tidalcycles/strudel/pull/919 -- 2 years blog post by @felixroos in https://github.com/tidalcycles/strudel/pull/929 +- Showcase by @felixroos in https://codeberg.org/uzu/strudel/pulls/885 +- Recipes by @felixroos in https://codeberg.org/uzu/strudel/pulls/742 +- Document striate function by @ilesinge in https://codeberg.org/uzu/strudel/pulls/766 +- Document adsr function by @ilesinge in https://codeberg.org/uzu/strudel/pulls/767 +- Add function params in reference tab by @ilesinge in https://codeberg.org/uzu/strudel/pulls/785 +- Update first-sounds.mdx by @bwagner in https://codeberg.org/uzu/strudel/pulls/794 +- Update recap.mdx by @bwagner in https://codeberg.org/uzu/strudel/pulls/797 +- Update pattern-effects.mdx by @bwagner in https://codeberg.org/uzu/strudel/pulls/796 +- Update first-effects.mdx by @bwagner in https://codeberg.org/uzu/strudel/pulls/795 +- Document pianoroll by @ilesinge in https://codeberg.org/uzu/strudel/pulls/784 +- Add doc for euclidLegatoRot, wordfall and slider by @ilesinge in https://codeberg.org/uzu/strudel/pulls/801 +- Improve documentation for synonym functions by @ilesinge in https://codeberg.org/uzu/strudel/pulls/800 +- Add and style algolia search by @ilesinge in https://codeberg.org/uzu/strudel/pulls/827 +- Fix a typo by @drewgbarnes in https://codeberg.org/uzu/strudel/pulls/830 +- add mastodon link by @felixroos in https://codeberg.org/uzu/strudel/pulls/884 +- adds a blog by @felixroos in https://codeberg.org/uzu/strudel/pulls/911 +- community bakery by @felixroos in https://codeberg.org/uzu/strudel/pulls/923 +- Blog improvements by @felixroos in https://codeberg.org/uzu/strudel/pulls/919 +- 2 years blog post by @felixroos in https://codeberg.org/uzu/strudel/pulls/929
@@ -124,34 +124,34 @@ Plenty of things have been added to the docs, including a [showcase of what peop
There is a lot more -- mini notation: international alphabets support by @ilesinge in https://github.com/tidalcycles/strudel/pull/751 -- Add shabda shortcut by @ilesinge in https://github.com/tidalcycles/strudel/pull/740 -- add play function by @felixroos in https://github.com/tidalcycles/strudel/pull/758 (superseded by next) -- tidal style d1 ... d9 functions + more by @felixroos in https://github.com/tidalcycles/strudel/pull/805 -- add vscode bindings by @Dsm0 in https://github.com/tidalcycles/strudel/pull/773 -- Implement optional hover tooltip with function documentation by @ilesinge in https://github.com/tidalcycles/strudel/pull/783 -- samples loading shortcuts: by @felixroos in https://github.com/tidalcycles/strudel/pull/788 -- add option to disable active line highlighting in Code Settings by @kasparsj in https://github.com/tidalcycles/strudel/pull/804 -- Color hsl by @felixroos in https://github.com/tidalcycles/strudel/pull/815 -- Patterns tab + Refactor Panel by @felixroos in https://github.com/tidalcycles/strudel/pull/769 -- patterns tab: import patterns + style by @felixroos in https://github.com/tidalcycles/strudel/pull/852 -- Export patterns + ui tweaks by @felixroos in https://github.com/tidalcycles/strudel/pull/855 -- Pattern organization by @felixroos in https://github.com/tidalcycles/strudel/pull/858 -- Sound Import from local file system by @daslyfe in https://github.com/tidalcycles/strudel/pull/839 -- bugfix: suspend and close existing audio context when changing interface by @daslyfe in https://github.com/tidalcycles/strudel/pull/882 -- add root mode for voicings by @felixroos in https://github.com/tidalcycles/strudel/pull/887 -- scales can now be anchored by @felixroos in https://github.com/tidalcycles/strudel/pull/888 -- add dough function for raw dsp by @felixroos in https://github.com/tidalcycles/strudel/pull/707 (experimental) -- support mininotation '..' range operator, fixes #715 by @yaxu in https://github.com/tidalcycles/strudel/pull/716 -- Add pick and squeeze functions by @daslyfe in https://github.com/tidalcycles/strudel/pull/771 -- support , in < > by @felixroos in https://github.com/tidalcycles/strudel/pull/886 -- public sharing by @felixroos in https://github.com/tidalcycles/strudel/pull/910 -- pick, pickmod, inhabit, inhabitmod by @yaxu in https://github.com/tidalcycles/strudel/pull/921 -- Mini-notation additions towards tidal compatibility by @yaxu in https://github.com/tidalcycles/strudel/pull/926 -- add pickF and pickmodF by @geikha in https://github.com/tidalcycles/strudel/pull/924 -- Make splice cps-aware by @yaxu in https://github.com/tidalcycles/strudel/pull/932 -- Refactor cps functions by @felixroos in https://github.com/tidalcycles/strudel/pull/933 -- Add useful pattern selection behavior for performing. by @daslyfe in https://github.com/tidalcycles/strudel/pull/897 +- mini notation: international alphabets support by @ilesinge in https://codeberg.org/uzu/strudel/pulls/751 +- Add shabda shortcut by @ilesinge in https://codeberg.org/uzu/strudel/pulls/740 +- add play function by @felixroos in https://codeberg.org/uzu/strudel/pulls/758 (superseded by next) +- tidal style d1 ... d9 functions + more by @felixroos in https://codeberg.org/uzu/strudel/pulls/805 +- add vscode bindings by @Dsm0 in https://codeberg.org/uzu/strudel/pulls/773 +- Implement optional hover tooltip with function documentation by @ilesinge in https://codeberg.org/uzu/strudel/pulls/783 +- samples loading shortcuts: by @felixroos in https://codeberg.org/uzu/strudel/pulls/788 +- add option to disable active line highlighting in Code Settings by @kasparsj in https://codeberg.org/uzu/strudel/pulls/804 +- Color hsl by @felixroos in https://codeberg.org/uzu/strudel/pulls/815 +- Patterns tab + Refactor Panel by @felixroos in https://codeberg.org/uzu/strudel/pulls/769 +- patterns tab: import patterns + style by @felixroos in https://codeberg.org/uzu/strudel/pulls/852 +- Export patterns + ui tweaks by @felixroos in https://codeberg.org/uzu/strudel/pulls/855 +- Pattern organization by @felixroos in https://codeberg.org/uzu/strudel/pulls/858 +- Sound Import from local file system by @daslyfe in https://codeberg.org/uzu/strudel/pulls/839 +- bugfix: suspend and close existing audio context when changing interface by @daslyfe in https://codeberg.org/uzu/strudel/pulls/882 +- add root mode for voicings by @felixroos in https://codeberg.org/uzu/strudel/pulls/887 +- scales can now be anchored by @felixroos in https://codeberg.org/uzu/strudel/pulls/888 +- add dough function for raw dsp by @felixroos in https://codeberg.org/uzu/strudel/pulls/707 (experimental) +- support mininotation '..' range operator, fixes #715 by @yaxu in https://codeberg.org/uzu/strudel/pulls/716 +- Add pick and squeeze functions by @daslyfe in https://codeberg.org/uzu/strudel/pulls/771 +- support , in < > by @felixroos in https://codeberg.org/uzu/strudel/pulls/886 +- public sharing by @felixroos in https://codeberg.org/uzu/strudel/pulls/910 +- pick, pickmod, inhabit, inhabitmod by @yaxu in https://codeberg.org/uzu/strudel/pulls/921 +- Mini-notation additions towards tidal compatibility by @yaxu in https://codeberg.org/uzu/strudel/pulls/926 +- add pickF and pickmodF by @geikha in https://codeberg.org/uzu/strudel/pulls/924 +- Make splice cps-aware by @yaxu in https://codeberg.org/uzu/strudel/pulls/932 +- Refactor cps functions by @felixroos in https://codeberg.org/uzu/strudel/pulls/933 +- Add useful pattern selection behavior for performing. by @daslyfe in https://codeberg.org/uzu/strudel/pulls/897
@@ -160,79 +160,79 @@ Plenty of things have been added to the docs, including a [showcase of what peop
show -- fix: finally repair envelopes by @felixroos in https://github.com/tidalcycles/strudel/pull/861 -- fix: reverb regenerate loophole by @felixroos in https://github.com/tidalcycles/strudel/pull/726 -- fix: reverb roomsize not required by @felixroos in https://github.com/tidalcycles/strudel/pull/731 -- fix: reverb sampleRate by @felixroos in https://github.com/tidalcycles/strudel/pull/732 -- consume n with scale by @felixroos in https://github.com/tidalcycles/strudel/pull/727 -- fix: hashes in urls by @felixroos in https://github.com/tidalcycles/strudel/pull/728 -- [Bug Fix] chooseWith: prevent pattern from stopping audio when selection is >= 1 or < 0 by @daslyfe in https://github.com/tidalcycles/strudel/pull/741 -- Fix addivite synthesis phases by @felixroos in https://github.com/tidalcycles/strudel/pull/762 -- fix: scale offset by @felixroos in https://github.com/tidalcycles/strudel/pull/764 -- fix zen mode logo overlap by @felixroos in https://github.com/tidalcycles/strudel/pull/760 -- fix: share copy to clipboard + alert by @felixroos in https://github.com/tidalcycles/strudel/pull/774 -- fix: style issues by @felixroos in https://github.com/tidalcycles/strudel/pull/781 -- Fix scope pos + document by @felixroos in https://github.com/tidalcycles/strudel/pull/786 -- don't use anchor links for reference by @felixroos in https://github.com/tidalcycles/strudel/pull/791 -- remove unwanted cm6 outline for strudelTheme by @kasparsj in https://github.com/tidalcycles/strudel/pull/802 -- FIXES: palindrome abc -> abccba by @bwagner in https://github.com/tidalcycles/strudel/pull/831 -- Bug Fix #119: Clock drift by @daslyfe in https://github.com/tidalcycles/strudel/pull/874 -- bugfix: sound select indexes out of bounds by @daslyfe in https://github.com/tidalcycles/strudel/pull/871 -- Error tolerance by @felixroos in https://github.com/tidalcycles/strudel/pull/880 -- fix: make sure n is never undefined before nanFallback by @felixroos in https://github.com/tidalcycles/strudel/pull/881 -- fix: invisible selection on vim + emacs mode by @felixroos in https://github.com/tidalcycles/strudel/pull/889 -- fix: autocomplete / tooltip code example bug by @felixroos in https://github.com/tidalcycles/strudel/pull/898 -- Fix examples page, piano() and a few workshop imgs by @shiyouganai in https://github.com/tidalcycles/strudel/pull/848 -- fix: trailing slash confusion by @felixroos in https://github.com/tidalcycles/strudel/pull/743 -- fix: try different trailing slash behavior by @felixroos in https://github.com/tidalcycles/strudel/pull/744 -- Fix krill build command in README by @ilesinge in https://github.com/tidalcycles/strudel/pull/748 -- Fix for #1. Enables named instruments for csoundm. by @gogins in https://github.com/tidalcycles/strudel/pull/662 -- fix: missing hash for links starting with / by @felixroos in https://github.com/tidalcycles/strudel/pull/845 -- fix: swatch png src by @felixroos in https://github.com/tidalcycles/strudel/pull/846 -- Fix edge case with rehype-urls and trailing slashes in image file paths by @shiyouganai in https://github.com/tidalcycles/strudel/pull/849 -- fix: multiple repls by @felixroos in https://github.com/tidalcycles/strudel/pull/813 -- Fix chunk, add fastChunk and repeatCycles by @yaxu in https://github.com/tidalcycles/strudel/pull/712 -- Update tauri.yml workflow file by @vasilymilovidov in https://github.com/tidalcycles/strudel/pull/705 -- vite-vanilla-repl readme fix by @felixroos in https://github.com/tidalcycles/strudel/pull/737 -- completely revert config mess by @felixroos in https://github.com/tidalcycles/strudel/pull/745 -- hopefully fix trailing slashes bug by @felixroos in https://github.com/tidalcycles/strudel/pull/753 -- Update vite pwa by @felixroos in https://github.com/tidalcycles/strudel/pull/772 -- Update to Astro 3 by @felixroos in https://github.com/tidalcycles/strudel/pull/775 -- support multiple named serial connections, change default baudrate by @yaxu in https://github.com/tidalcycles/strudel/pull/551 -- CHANGES: github action checkout v2 -> v4 by @bwagner in https://github.com/tidalcycles/strudel/pull/837 -- CHANGES: pin pnpm to version 8.3.1 by @bwagner in https://github.com/tidalcycles/strudel/pull/834 -- CHANGES: github action pnpm version from 7 to 8.3.1 by @bwagner in https://github.com/tidalcycles/strudel/pull/835 -- ADDS: JetBrains IDE files and directories to .gitignore by @bwagner in https://github.com/tidalcycles/strudel/pull/840 -- Prevent 404 on Algolia crawls by @ilesinge in https://github.com/tidalcycles/strudel/pull/838 -- Add in fixes from my fork to slashocalypse branch by @shiyouganai in https://github.com/tidalcycles/strudel/pull/843 -- improve slashing + base href behavior by @felixroos in https://github.com/tidalcycles/strudel/pull/842 -- CHANGES: pnpm 8.1.3 to 8.11.0 by @bwagner in https://github.com/tidalcycles/strudel/pull/850 -- add missing trailing slashes by @felixroos in https://github.com/tidalcycles/strudel/pull/860 -- move all examples to separate examples folder by @felixroos in https://github.com/tidalcycles/strudel/pull/878 -- Dependency update by @felixroos in https://github.com/tidalcycles/strudel/pull/879 -- Update Vite version so hot reload works properly with newest pnpm version by @daslyfe in https://github.com/tidalcycles/strudel/pull/892 -- prevent vite from complaining about additional exports in jsx files by @daslyfe in https://github.com/tidalcycles/strudel/pull/891 -- fix some build warnings by @felixroos in https://github.com/tidalcycles/strudel/pull/902 -- Remove hideHeader for better mobile UI and consistency by @rjulian in https://github.com/tidalcycles/strudel/pull/894 -- Fix: swatch/[name].png.js static path by @oscarbyrne in https://github.com/tidalcycles/strudel/pull/916 -- rename @strudel.cycles/_ packages to @strudel/_ by @felixroos in https://github.com/tidalcycles/strudel/pull/917 -- `pick` now accepts lookup tables, with alternate cycle squeezing behaviour as new `inhabit` function by @yaxu in https://github.com/tidalcycles/strudel/pull/918 -- Revert "`pick` now accepts lookup tables, with alternate cycle squeezing behaviour as new `inhabit` function" by @yaxu in https://github.com/tidalcycles/strudel/pull/920 -- Fix pattern tab not showing patterns without created date by @daslyfe in https://github.com/tidalcycles/strudel/pull/934 +- fix: finally repair envelopes by @felixroos in https://codeberg.org/uzu/strudel/pulls/861 +- fix: reverb regenerate loophole by @felixroos in https://codeberg.org/uzu/strudel/pulls/726 +- fix: reverb roomsize not required by @felixroos in https://codeberg.org/uzu/strudel/pulls/731 +- fix: reverb sampleRate by @felixroos in https://codeberg.org/uzu/strudel/pulls/732 +- consume n with scale by @felixroos in https://codeberg.org/uzu/strudel/pulls/727 +- fix: hashes in urls by @felixroos in https://codeberg.org/uzu/strudel/pulls/728 +- [Bug Fix] chooseWith: prevent pattern from stopping audio when selection is >= 1 or < 0 by @daslyfe in https://codeberg.org/uzu/strudel/pulls/741 +- Fix addivite synthesis phases by @felixroos in https://codeberg.org/uzu/strudel/pulls/762 +- fix: scale offset by @felixroos in https://codeberg.org/uzu/strudel/pulls/764 +- fix zen mode logo overlap by @felixroos in https://codeberg.org/uzu/strudel/pulls/760 +- fix: share copy to clipboard + alert by @felixroos in https://codeberg.org/uzu/strudel/pulls/774 +- fix: style issues by @felixroos in https://codeberg.org/uzu/strudel/pulls/781 +- Fix scope pos + document by @felixroos in https://codeberg.org/uzu/strudel/pulls/786 +- don't use anchor links for reference by @felixroos in https://codeberg.org/uzu/strudel/pulls/791 +- remove unwanted cm6 outline for strudelTheme by @kasparsj in https://codeberg.org/uzu/strudel/pulls/802 +- FIXES: palindrome abc -> abccba by @bwagner in https://codeberg.org/uzu/strudel/pulls/831 +- Bug Fix #119: Clock drift by @daslyfe in https://codeberg.org/uzu/strudel/pulls/874 +- bugfix: sound select indexes out of bounds by @daslyfe in https://codeberg.org/uzu/strudel/pulls/871 +- Error tolerance by @felixroos in https://codeberg.org/uzu/strudel/pulls/880 +- fix: make sure n is never undefined before nanFallback by @felixroos in https://codeberg.org/uzu/strudel/pulls/881 +- fix: invisible selection on vim + emacs mode by @felixroos in https://codeberg.org/uzu/strudel/pulls/889 +- fix: autocomplete / tooltip code example bug by @felixroos in https://codeberg.org/uzu/strudel/pulls/898 +- Fix examples page, piano() and a few workshop imgs by @shiyouganai in https://codeberg.org/uzu/strudel/pulls/848 +- fix: trailing slash confusion by @felixroos in https://codeberg.org/uzu/strudel/pulls/743 +- fix: try different trailing slash behavior by @felixroos in https://codeberg.org/uzu/strudel/pulls/744 +- Fix krill build command in README by @ilesinge in https://codeberg.org/uzu/strudel/pulls/748 +- Fix for #1. Enables named instruments for csoundm. by @gogins in https://codeberg.org/uzu/strudel/pulls/662 +- fix: missing hash for links starting with / by @felixroos in https://codeberg.org/uzu/strudel/pulls/845 +- fix: swatch png src by @felixroos in https://codeberg.org/uzu/strudel/pulls/846 +- Fix edge case with rehype-urls and trailing slashes in image file paths by @shiyouganai in https://codeberg.org/uzu/strudel/pulls/849 +- fix: multiple repls by @felixroos in https://codeberg.org/uzu/strudel/pulls/813 +- Fix chunk, add fastChunk and repeatCycles by @yaxu in https://codeberg.org/uzu/strudel/pulls/712 +- Update tauri.yml workflow file by @vasilymilovidov in https://codeberg.org/uzu/strudel/pulls/705 +- vite-vanilla-repl readme fix by @felixroos in https://codeberg.org/uzu/strudel/pulls/737 +- completely revert config mess by @felixroos in https://codeberg.org/uzu/strudel/pulls/745 +- hopefully fix trailing slashes bug by @felixroos in https://codeberg.org/uzu/strudel/pulls/753 +- Update vite pwa by @felixroos in https://codeberg.org/uzu/strudel/pulls/772 +- Update to Astro 3 by @felixroos in https://codeberg.org/uzu/strudel/pulls/775 +- support multiple named serial connections, change default baudrate by @yaxu in https://codeberg.org/uzu/strudel/pulls/551 +- CHANGES: github action checkout v2 -> v4 by @bwagner in https://codeberg.org/uzu/strudel/pulls/837 +- CHANGES: pin pnpm to version 8.3.1 by @bwagner in https://codeberg.org/uzu/strudel/pulls/834 +- CHANGES: github action pnpm version from 7 to 8.3.1 by @bwagner in https://codeberg.org/uzu/strudel/pulls/835 +- ADDS: JetBrains IDE files and directories to .gitignore by @bwagner in https://codeberg.org/uzu/strudel/pulls/840 +- Prevent 404 on Algolia crawls by @ilesinge in https://codeberg.org/uzu/strudel/pulls/838 +- Add in fixes from my fork to slashocalypse branch by @shiyouganai in https://codeberg.org/uzu/strudel/pulls/843 +- improve slashing + base href behavior by @felixroos in https://codeberg.org/uzu/strudel/pulls/842 +- CHANGES: pnpm 8.1.3 to 8.11.0 by @bwagner in https://codeberg.org/uzu/strudel/pulls/850 +- add missing trailing slashes by @felixroos in https://codeberg.org/uzu/strudel/pulls/860 +- move all examples to separate examples folder by @felixroos in https://codeberg.org/uzu/strudel/pulls/878 +- Dependency update by @felixroos in https://codeberg.org/uzu/strudel/pulls/879 +- Update Vite version so hot reload works properly with newest pnpm version by @daslyfe in https://codeberg.org/uzu/strudel/pulls/892 +- prevent vite from complaining about additional exports in jsx files by @daslyfe in https://codeberg.org/uzu/strudel/pulls/891 +- fix some build warnings by @felixroos in https://codeberg.org/uzu/strudel/pulls/902 +- Remove hideHeader for better mobile UI and consistency by @rjulian in https://codeberg.org/uzu/strudel/pulls/894 +- Fix: swatch/[name].png.js static path by @oscarbyrne in https://codeberg.org/uzu/strudel/pulls/916 +- rename @strudel.cycles/_ packages to @strudel/_ by @felixroos in https://codeberg.org/uzu/strudel/pulls/917 +- `pick` now accepts lookup tables, with alternate cycle squeezing behaviour as new `inhabit` function by @yaxu in https://codeberg.org/uzu/strudel/pulls/918 +- Revert "`pick` now accepts lookup tables, with alternate cycle squeezing behaviour as new `inhabit` function" by @yaxu in https://codeberg.org/uzu/strudel/pulls/920 +- Fix pattern tab not showing patterns without created date by @daslyfe in https://codeberg.org/uzu/strudel/pulls/934
## New Contributors -- @ilesinge made their first contribution in https://github.com/tidalcycles/strudel/pull/748 -- @Dsm0 made their first contribution in https://github.com/tidalcycles/strudel/pull/773 -- @kasparsj made their first contribution in https://github.com/tidalcycles/strudel/pull/802 -- @atfornes made their first contribution in https://github.com/tidalcycles/strudel/pull/818 -- @drewgbarnes made their first contribution in https://github.com/tidalcycles/strudel/pull/830 -- @shiyouganai made their first contribution in https://github.com/tidalcycles/strudel/pull/843 -- @rjulian made their first contribution in https://github.com/tidalcycles/strudel/pull/894 -- @fnordomat made their first contribution in https://github.com/tidalcycles/strudel/pull/907 -- @oscarbyrne made their first contribution in https://github.com/tidalcycles/strudel/pull/916 -- @geikha made their first contribution in https://github.com/tidalcycles/strudel/pull/924 +- @ilesinge made their first contribution in https://codeberg.org/uzu/strudel/pulls/748 +- @Dsm0 made their first contribution in https://codeberg.org/uzu/strudel/pulls/773 +- @kasparsj made their first contribution in https://codeberg.org/uzu/strudel/pulls/802 +- @atfornes made their first contribution in https://codeberg.org/uzu/strudel/pulls/818 +- @drewgbarnes made their first contribution in https://codeberg.org/uzu/strudel/pulls/830 +- @shiyouganai made their first contribution in https://codeberg.org/uzu/strudel/pulls/843 +- @rjulian made their first contribution in https://codeberg.org/uzu/strudel/pulls/894 +- @fnordomat made their first contribution in https://codeberg.org/uzu/strudel/pulls/907 +- @oscarbyrne made their first contribution in https://codeberg.org/uzu/strudel/pulls/916 +- @geikha made their first contribution in https://codeberg.org/uzu/strudel/pulls/924 -**Full Changelog**: https://github.com/tidalcycles/strudel/compare/v0.9.0...v1.0.0 +**Full Changelog**: https://codeberg.org/uzu/strudel/compare/v0.9.0...v1.0.0 diff --git a/website/src/content/blog/year-2.mdx b/website/src/content/blog/year-2.mdx index 38e39bbfd..0ea4616a3 100644 --- a/website/src/content/blog/year-2.mdx +++ b/website/src/content/blog/year-2.mdx @@ -216,9 +216,9 @@ Here is a recording of the first session we organized over the discord server: The midi integration has gotten a few new features: -- [clock out](https://github.com/tidalcycles/strudel/pull/710) to sync your midi devices / DAW to the strudel clock (better doc coming soon) +- [clock out](https://codeberg.org/uzu/strudel/pulls/710) to sync your midi devices / DAW to the strudel clock (better doc coming soon) - [cc output](https://strudel.cc/learn/input-output/#ccn--ccv) to send cc values to your gear -- [cc input](https://github.com/tidalcycles/strudel/pull/699) to control strudel via MIDI (better doc coming soon) +- [cc input](https://codeberg.org/uzu/strudel/pulls/699) to control strudel via MIDI (better doc coming soon) Here is a little demo of me fiddling with a midi controller, changing a piano pattern: From 912b3270aaaf1a28f2444f130c179bf110edc048 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 14 Jun 2025 14:35:19 +0200 Subject: [PATCH 126/174] use yt for gh video assets + replace release asset links --- .../content/blog/release-0.8.0-himbeermuffin.mdx | 14 +++++++------- .../src/content/blog/release-0.9.0-bananenbrot.mdx | 6 +++--- .../blog/release-1.0.0-geburtstagskuchen.mdx | 2 -- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/website/src/content/blog/release-0.8.0-himbeermuffin.mdx b/website/src/content/blog/release-0.8.0-himbeermuffin.mdx index babab5954..1eb1888a7 100644 --- a/website/src/content/blog/release-0.8.0-himbeermuffin.mdx +++ b/website/src/content/blog/release-0.8.0-himbeermuffin.mdx @@ -6,7 +6,7 @@ tags: ['meta'] author: froos --- -import BlogVideo from '../../components/BlogVideo.astro'; +import { Youtube } from '@src/components/Youtube'; These are the release notes for Strudel 0.8.0 aka "Himbeermuffin"! @@ -18,11 +18,11 @@ Let me write up some of the highlights: Besides the REPL (https://strudel.tidalcycles.org/), Strudel is now also distributed as a Desktop App via https://tauri.app/! Thanks to [vasilymilovidov](https://github.com/vasilymilovidov)! -- [Linux: Debian based](https://github.com/tidalcycles/strudel/releases/download/v0.8.0/strudel_0.1.0_amd64.deb) -- [Linux: AppImage](https://github.com/tidalcycles/strudel/releases/download/v0.8.0/strudel_0.1.0_amd64.AppImage) -- [MacOS](https://github.com/tidalcycles/strudel/releases/download/v0.8.0/Strudel_0.1.0_x64.dmg) -- [Windows .exe](https://github.com/tidalcycles/strudel/releases/download/v0.8.0/Strudel_0.1.0_x64-setup.exe) -- [Windows .msi](https://github.com/tidalcycles/strudel/releases/download/v0.8.0/Strudel_0.1.0_x64_en-US.msi) +- [Linux: Debian based](https://codeberg.org/uzu/strudel/releases/download/v0.8.0/strudel_0.1.0_amd64.deb) +- [Linux: AppImage](https://codeberg.org/uzu/strudel/releases/download/v0.8.0/strudel_0.1.0_amd64.AppImage) +- [MacOS](https://codeberg.org/uzu/strudel/releases/download/v0.8.0/Strudel_0.1.0_x64.dmg) +- [Windows .exe](https://codeberg.org/uzu/strudel/releases/download/v0.8.0/Strudel_0.1.0_x64-setup.exe) +- [Windows .msi](https://codeberg.org/uzu/strudel/releases/download/v0.8.0/Strudel_0.1.0_x64_en-US.msi) edit: the desktop app performance on linux is currently not that great.. the web REPL runs much smoother (using firefox or chromium) @@ -43,7 +43,7 @@ I would be very happy to collect some feedback on how it works across different Also still undocumented, but you can now visualize patterns as a spiral via `.spiral()`: - + This is especially nice because strudel is not only the name of a dessert but also the german word for vortex! The spiral is very fitting to visualize cycles because you can align cycles vertically, while surfing along an infinite twisted timeline. diff --git a/website/src/content/blog/release-0.9.0-bananenbrot.mdx b/website/src/content/blog/release-0.9.0-bananenbrot.mdx index 944675e38..eb6894f69 100644 --- a/website/src/content/blog/release-0.9.0-bananenbrot.mdx +++ b/website/src/content/blog/release-0.9.0-bananenbrot.mdx @@ -6,8 +6,6 @@ tags: ['meta'] author: froos --- -import BlogVideo from '../../components/BlogVideo.astro'; - These are the release notes for Strudel 0.9.0 aka "Bananenbrot"! The last release was over 11 weeks ago, so a lot of things have happened! @@ -25,7 +23,9 @@ Main new features include: - [vibrato](https://strudel.tidalcycles.org/learn/synths#vibrato) - an integration of [ZZFX](https://strudel.tidalcycles.org/learn/synths#zzfx) - +import { Youtube } from '@src/components/Youtube'; + + Related PRs: diff --git a/website/src/content/blog/release-1.0.0-geburtstagskuchen.mdx b/website/src/content/blog/release-1.0.0-geburtstagskuchen.mdx index 0ee149a20..bfbf24a20 100644 --- a/website/src/content/blog/release-1.0.0-geburtstagskuchen.mdx +++ b/website/src/content/blog/release-1.0.0-geburtstagskuchen.mdx @@ -6,8 +6,6 @@ tags: ['meta'] author: froos --- -import BlogVideo from '../../components/BlogVideo.astro'; - These are the release notes for Strudel 1.0.0 aka "Geburtstagskuchen" This release marks the 2 year anniversary of the project, the first commit was on the 22nd January 2022 by Alex McLean. From 0163c8ba25712f56c0aa725a770d8c84a3b6552f Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 14 Jun 2025 14:47:16 +0200 Subject: [PATCH 127/174] replace remaining links to old github repo --- website/src/content/blog/year-2.mdx | 2 +- website/src/pages/learn/getting-started.mdx | 2 +- website/src/pages/learn/input-output.mdx | 4 +-- website/src/pages/learn/metadata.mdx | 2 -- website/src/pages/learn/samples.mdx | 2 +- website/src/pages/learn/strudel-vs-tidal.mdx | 8 ++--- website/src/pages/technical-manual/docs.mdx | 4 +-- .../src/pages/technical-manual/packages.mdx | 30 +++++++++---------- .../pages/technical-manual/project-start.mdx | 6 ++-- website/src/pages/technical-manual/repl.mdx | 4 +-- 10 files changed, 31 insertions(+), 33 deletions(-) diff --git a/website/src/content/blog/year-2.mdx b/website/src/content/blog/year-2.mdx index 0ea4616a3..1a33c673b 100644 --- a/website/src/content/blog/year-2.mdx +++ b/website/src/content/blog/year-2.mdx @@ -228,7 +228,7 @@ Here is a little demo of me fiddling with a midi controller, changing a piano pa - You can now run [hydra inside strudel](https://strudel.cc/learn/hydra/) + you can even run strudel in [hydra](https://hydra.ojack.xyz), thanks to [Olivia Jack](https://ojack.xyz/) and [Ámbar Tenorio Fornés](https://atenor.io/)! Read more [here](https://alpaca.pubpub.org/pub/b7hwrjfk/release/2?readingCollection=1def0192) - There is now a [VSCode Plugin](https://marketplace.visualstudio.com/items?itemName=roipoussiere.tidal-strudel) thanks to [roipoussiere](https://github.com/roipoussiere)! It allows you run patterns from a `.strudel` file inside VSCode. -- Strudel can now be downloaded as a desktop app, thanks to [vasilymilovidov](https://github.com/vasilymilovidov) who has wrapped the REPL with [tauri](https://tauri.app/). You can download it [on the releases page](https://github.com/tidalcycles/strudel/releases) (scroll down to Assets). The performance is not optimal on MacOS and Linux, which is why it is still considered experimental +- Strudel can now be downloaded as a desktop app, thanks to [vasilymilovidov](https://github.com/vasilymilovidov) who has wrapped the REPL with [tauri](https://tauri.app/). You can download it [on the releases page](https://codeberg.org/uzu/strudel/releases) (scroll down to Assets). The performance is not optimal on MacOS and Linux, which is why it is still considered experimental ## Stats diff --git a/website/src/pages/learn/getting-started.mdx b/website/src/pages/learn/getting-started.mdx index d18a05607..2d056d3b4 100644 --- a/website/src/pages/learn/getting-started.mdx +++ b/website/src/pages/learn/getting-started.mdx @@ -81,7 +81,7 @@ s("bd,[~ ],hh*8") // drums Please note that this project is still in its experimental state. In the future, parts of it might change significantly. This tutorial is also far from complete. -You can contribute to it clicking 'Edit this page' in the top right, or by visiting the [Strudel GitHub page](https://github.com/tidalcycles/strudel/). +You can contribute to it clicking 'Edit this page' in the top right, or by visiting the [Strudel GitHub page](https://codeberg.org/uzu/strudel/). # What's next? diff --git a/website/src/pages/learn/input-output.mdx b/website/src/pages/learn/input-output.mdx index 92379aa6f..0dd48b7de 100644 --- a/website/src/pages/learn/input-output.mdx +++ b/website/src/pages/learn/input-output.mdx @@ -8,7 +8,7 @@ import { JsDoc } from '../../docs/JsDoc'; # MIDI, OSC and MQTT -Normally, Strudel is used to pattern sound, using its own '[web audio](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API)'-based synthesiser called [SuperDough](https://github.com/tidalcycles/strudel/tree/main/packages/superdough). +Normally, Strudel is used to pattern sound, using its own '[web audio](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API)'-based synthesiser called [SuperDough](https://codeberg.org/uzu/strudel/src/branch/main/packages/superdough). It is also possible to pattern other things with Strudel, such as software and hardware synthesisers with MIDI, other software using Open Sound Control/OSC (including the [SuperDirt](https://github.com/musikinformatik/SuperDirt/) synthesiser commonly used with Strudel's sibling [TidalCycles](https://tidalcycles.org/)), or the MQTT 'internet of things' protocol. @@ -184,7 +184,7 @@ To get SuperDirt to work with Strudel, you need to 1. install SuperCollider + sc3 plugins, see [Tidal Docs](https://tidalcycles.org/docs/) (Install Tidal) for more info. 2. install SuperDirt, or the [StrudelDirt](https://github.com/daslyfe/StrudelDirt) fork which is optimised for use with Strudel 3. install [node.js](https://nodejs.org/en/) -4. download [Strudel Repo](https://github.com/tidalcycles/strudel/) (or git clone, if you have git installed) +4. download [Strudel Repo](https://codeberg.org/uzu/strudel/) (or git clone, if you have git installed) 5. run `pnpm i` in the strudel directory 6. run `pnpm run osc` to start the osc server, which forwards OSC messages from Strudel REPL to SuperCollider diff --git a/website/src/pages/learn/metadata.mdx b/website/src/pages/learn/metadata.mdx index e8bb86dfc..61db16f0e 100644 --- a/website/src/pages/learn/metadata.mdx +++ b/website/src/pages/learn/metadata.mdx @@ -18,8 +18,6 @@ You can optionally add some music metadata in your Strudel code, by using tags i Like other comments, those are ignored by Strudel, but it can be used by other tools to retrieve some information about the music. -It is for instance used by the [swatch tool](https://github.com/tidalcycles/strudel/tree/main/my-patterns) to display pattern titles in the [examples page](https://strudel.cc/examples/). - ## Alternative syntax You can also use comment blocks: diff --git a/website/src/pages/learn/samples.mdx b/website/src/pages/learn/samples.mdx index 890de3aae..cd7944caa 100644 --- a/website/src/pages/learn/samples.mdx +++ b/website/src/pages/learn/samples.mdx @@ -62,7 +62,7 @@ To see which sample names are available, open the `sounds` tab in the [REPL](htt Note that only the sample maps (mapping names to URLs) are loaded initially, while the audio samples themselves are not loaded until they are actually played. This behaviour of loading things only when they are needed is also called `lazy loading`. While it saves resources, it can also lead to sounds not being audible the first time they are triggered, because the sound is still loading. -[This might be fixed in the future](https://github.com/tidalcycles/strudel/issues/187) +[This might be fixed in the future](https://codeberg.org/uzu/strudel/issues/187) # Sound Banks diff --git a/website/src/pages/learn/strudel-vs-tidal.mdx b/website/src/pages/learn/strudel-vs-tidal.mdx index 40ea188fd..9c1cc6b73 100644 --- a/website/src/pages/learn/strudel-vs-tidal.mdx +++ b/website/src/pages/learn/strudel-vs-tidal.mdx @@ -79,7 +79,7 @@ Instead of `+` / `add`, you can use any of the available operators of the first ## Function Compatibility -[This issue](https://github.com/tidalcycles/strudel/issues/31) tracks which Tidal functions are implemented in Strudel. +[This issue](https://codeberg.org/uzu/strudel/issues/31) tracks which Tidal functions are implemented in Strudel. The list might not be 100% up to date and probably also misses some functions completely.. Feel encouraged to search the source code for a function you're looking for. If you find a function that's not on the list, please tell! @@ -89,7 +89,7 @@ If you find a function that's not on the list, please tell! As seen in the example, the `#` operator (shorthand for `|>`) is also just a function call in strudel. So `note "c5" # s "gtr"` becomes `note("c5").s('gtr')`. -[This file](https://github.com/tidalcycles/strudel/blob/main/packages/core/controls.mjs) lists all available control params. +[This file](https://codeberg.org/uzu/strudel/src/branch/main/packages/core/controls.mjs) lists all available control params. Note that not all of those work in the Webaudio Output of Strudel. If you find a tidal control that's not on the list, please tell! @@ -107,11 +107,11 @@ You can find a [list of available effects here](/learn/effects/). ### Sampler Strudel's sampler supports [a subset](/learn/samples) of Superdirt's sampler. -Also, samples are always loaded from a URL rather than from the disk, although [that might be possible in the future](https://github.com/tidalcycles/strudel/issues/118). +Also, samples are always loaded from a URL rather than from the disk, although [that might be possible in the future](https://codeberg.org/uzu/strudel/issues/118). ## Evaluation -The Strudel REPL does not support [block based evaluation](https://github.com/tidalcycles/strudel/issues/34) yet. +The Strudel REPL does not support [block based evaluation](https://codeberg.org/uzu/strudel/issues/34) yet. You can use labeled statements and `_` to mute: *8").scale('G4 minor') This will load the strudel website in an iframe, using the code provided within the HTML comments ``. The HTML comments are needed to make sure the browser won't interpret it as HTML. -For alternative ways to load this package, see the [@strudel/embed README](https://github.com/tidalcycles/strudel/tree/main/packages/embed#strudelembed). +For alternative ways to load this package, see the [@strudel/embed README](https://codeberg.org/uzu/strudel/src/branch/main/packages/embed#strudel-embed). ### @strudel/repl @@ -99,7 +99,7 @@ The upside of using the repl without an iframe is that you can pin the strudel v This will guarantee your pattern wont break due to changes to the strudel project in the future. -For more info on this package, see the [@strudel/repl README](https://github.com/tidalcycles/strudel/tree/main/packages/repl#strudelrepl). +For more info on this package, see the [@strudel/repl README](https://codeberg.org/uzu/strudel/src/branch/main/packages/repl#strudel-repl). ## With your own UI @@ -118,7 +118,7 @@ If you'd rather use your own UI, you can use the `@strudel/web` package: ``` -For more info on this package, see the [@strudel/web README](https://github.com/tidalcycles/strudel/tree/main/packages/web#strudelweb). +For more info on this package, see the [@strudel/web README]https://codeberg.org/uzu/strudel/src/branch/main/packages/web#strudel-web). ## Via npm diff --git a/website/src/pages/technical-manual/repl.mdx b/website/src/pages/technical-manual/repl.mdx index f336ce361..f53efac41 100644 --- a/website/src/pages/technical-manual/repl.mdx +++ b/website/src/pages/technical-manual/repl.mdx @@ -17,7 +17,7 @@ Besides a UI for playback control and meta information, the main part of the REP 2. While the REPL is running, the `Scheduler` queries the active `Pattern` by a regular interval, generating `Events` (also known as `Haps` in Strudel) for the next time span. 3. For each scheduling tick, all generated `Events` are triggered by calling their `onTrigger` method, which is set by the output. - + ## User Code @@ -75,7 +75,7 @@ The sum of both is passed to `withLoc` to tell each element its location, which Another important part of the user code is the mini notation, which allows to express rhythms in a short manner. -- the mini notation is [implemented as a PEG grammar](https://github.com/tidalcycles/strudel/blob/main/packages/mini/krill.pegjs), living in the [mini package](https://github.com/tidalcycles/strudel/tree/main/packages/mini) +- the mini notation is [implemented as a PEG grammar](https://codeberg.org/uzu/strudel/src/branch/talk/packages/mini/krill.pegjs), living in the [mini package](https://codeberg.org/uzu/strudel/src/branch/main/packages/mini) - it is based on [krill](https://github.com/Mdashdotdashn/krill) by Mdashdotdashn - the peg grammar is used to generate a parser with [peggyjs](https://peggyjs.org/) - the generated parser takes a mini notation string and outputs an AST From a7f044e1aae575355d8a92dea3ca05ef480a8c64 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 14 Jun 2025 16:17:22 +0200 Subject: [PATCH 128/174] more degithubbing --- packages/core/test/controls.test.mjs | 2 +- packages/core/test/drawLine.test.mjs | 2 +- packages/core/test/fraction.test.mjs | 2 +- packages/core/test/pattern.test.mjs | 2 +- packages/core/test/util.test.mjs | 2 +- packages/mini/test/mini.test.mjs | 2 +- packages/repl/README.md | 2 +- packages/tonal/test/tonal.test.mjs | 2 +- packages/tonal/test/tonleiter.test.mjs | 2 +- packages/transpiler/test/transpiler.test.mjs | 2 +- packages/xen/test/xen.test.mjs | 2 +- technical.manual.md | 6 +++--- website/src/cx.mjs | 2 +- website/src/repl/Repl.jsx | 2 +- website/src/repl/components/panel/WelcomeTab.jsx | 2 +- website/src/repl/tunes.mjs | 2 +- website/src/repl/useReplContext.jsx | 2 +- 17 files changed, 19 insertions(+), 19 deletions(-) diff --git a/packages/core/test/controls.test.mjs b/packages/core/test/controls.test.mjs index f0af02eb6..667c3a4e5 100644 --- a/packages/core/test/controls.test.mjs +++ b/packages/core/test/controls.test.mjs @@ -1,6 +1,6 @@ /* controls.test.mjs - -Copyright (C) 2023 Strudel contributors - see +Copyright (C) 2023 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ diff --git a/packages/core/test/drawLine.test.mjs b/packages/core/test/drawLine.test.mjs index 84359a7cf..2240080cd 100644 --- a/packages/core/test/drawLine.test.mjs +++ b/packages/core/test/drawLine.test.mjs @@ -1,6 +1,6 @@ /* drawLine.test.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ diff --git a/packages/core/test/fraction.test.mjs b/packages/core/test/fraction.test.mjs index 6b710d043..b167c9483 100644 --- a/packages/core/test/fraction.test.mjs +++ b/packages/core/test/fraction.test.mjs @@ -1,6 +1,6 @@ /* fraction.test.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs index 08611032c..696f13fef 100644 --- a/packages/core/test/pattern.test.mjs +++ b/packages/core/test/pattern.test.mjs @@ -1,6 +1,6 @@ /* pattern.test.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ diff --git a/packages/core/test/util.test.mjs b/packages/core/test/util.test.mjs index a511e0fc2..d3033a82c 100644 --- a/packages/core/test/util.test.mjs +++ b/packages/core/test/util.test.mjs @@ -1,6 +1,6 @@ /* util.test.mjs - Tests for the core 'util' module -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ diff --git a/packages/mini/test/mini.test.mjs b/packages/mini/test/mini.test.mjs index 15d501111..c38997308 100644 --- a/packages/mini/test/mini.test.mjs +++ b/packages/mini/test/mini.test.mjs @@ -1,6 +1,6 @@ /* mini.test.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ diff --git a/packages/repl/README.md b/packages/repl/README.md index 46819712c..3db4cc3f7 100644 --- a/packages/repl/README.md +++ b/packages/repl/README.md @@ -91,7 +91,7 @@ or ``` -The `.editor` property on the `strudel-editor` web component gives you the instance of [StrudelMirror](https://github.com/tidalcycles/strudel/blob/a46bd9b36ea7d31c9f1d3fca484297c7da86893f/packages/codemirror/codemirror.mjs#L124) that runs the REPL. +The `.editor` property on the `strudel-editor` web component gives you the instance of [StrudelMirror](https://codeberg.org/uzu/strudel/src/branch/a46bd9b36ea7d31c9f1d3fca484297c7da86893f/packages/codemirror/codemirror.mjs#L124) that runs the REPL. For example, you could use `setCode` to change the code from the outside, `start` / `stop` to toggle playback or `evaluate` to evaluate the code. diff --git a/packages/tonal/test/tonal.test.mjs b/packages/tonal/test/tonal.test.mjs index 8dd0e1861..c1155c238 100644 --- a/packages/tonal/test/tonal.test.mjs +++ b/packages/tonal/test/tonal.test.mjs @@ -1,6 +1,6 @@ /* tonal.test.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ diff --git a/packages/tonal/test/tonleiter.test.mjs b/packages/tonal/test/tonleiter.test.mjs index e1a693579..b53a8f405 100644 --- a/packages/tonal/test/tonleiter.test.mjs +++ b/packages/tonal/test/tonleiter.test.mjs @@ -1,6 +1,6 @@ /* tonleiter.test.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ diff --git a/packages/transpiler/test/transpiler.test.mjs b/packages/transpiler/test/transpiler.test.mjs index 988479b4f..02970cf43 100644 --- a/packages/transpiler/test/transpiler.test.mjs +++ b/packages/transpiler/test/transpiler.test.mjs @@ -1,6 +1,6 @@ /* transpiler.test.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ diff --git a/packages/xen/test/xen.test.mjs b/packages/xen/test/xen.test.mjs index 977b0f694..a0982208a 100644 --- a/packages/xen/test/xen.test.mjs +++ b/packages/xen/test/xen.test.mjs @@ -1,6 +1,6 @@ /* xen.test.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ diff --git a/technical.manual.md b/technical.manual.md index 58f7cd934..6068b48fe 100644 --- a/technical.manual.md +++ b/technical.manual.md @@ -17,7 +17,7 @@ More info in the [REPL README](https://codeberg.org/uzu/strudel/src/branch/main/ # High Level Overview - + ## 1. End User Code @@ -48,7 +48,7 @@ mini('c3 [e3 g3]') This is how it works: - + - The user code is parsed with a [shift parser](https://github.com/shapesecurity/shift-parser-js), generating an AST - The AST is transformed to resolve the syntax sugar @@ -171,7 +171,7 @@ Here is an example Hap value with different properties: ```js { note: 'a4', s: 'sawtooth', gain: 0.5, cutoff: 267 } ``` - +
diff --git a/website/src/cx.mjs b/website/src/cx.mjs index 4e4aea08d..d8ff9eda1 100644 --- a/website/src/cx.mjs +++ b/website/src/cx.mjs @@ -1,6 +1,6 @@ /* cx.js - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ diff --git a/website/src/repl/Repl.jsx b/website/src/repl/Repl.jsx index e46eb7171..813f63897 100644 --- a/website/src/repl/Repl.jsx +++ b/website/src/repl/Repl.jsx @@ -1,6 +1,6 @@ /* Repl.jsx - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ diff --git a/website/src/repl/components/panel/WelcomeTab.jsx b/website/src/repl/components/panel/WelcomeTab.jsx index a43f02ad4..b3b167c74 100644 --- a/website/src/repl/components/panel/WelcomeTab.jsx +++ b/website/src/repl/components/panel/WelcomeTab.jsx @@ -38,7 +38,7 @@ export function WelcomeTab({ context }) { , which is a popular live coding language for music, written in Haskell. Strudel is free/open source software: you can redistribute and/or modify it under the terms of the{' '} - + GNU Affero General Public License . You can find the source code at{' '} diff --git a/website/src/repl/tunes.mjs b/website/src/repl/tunes.mjs index cffe0c0eb..774e04e49 100644 --- a/website/src/repl/tunes.mjs +++ b/website/src/repl/tunes.mjs @@ -1,6 +1,6 @@ /* tunes.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ diff --git a/website/src/repl/useReplContext.jsx b/website/src/repl/useReplContext.jsx index 36e8099cb..12621c50a 100644 --- a/website/src/repl/useReplContext.jsx +++ b/website/src/repl/useReplContext.jsx @@ -1,6 +1,6 @@ /* Repl.jsx - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ From d856e567c17c04cad083850955d838e3e328e404 Mon Sep 17 00:00:00 2001 From: alex Date: Sat, 14 Jun 2025 15:22:27 +0100 Subject: [PATCH 129/174] link back to tech manual in wiki --- packages/README.md | 2 +- technical.manual.md | 193 -------------------------------------------- 2 files changed, 1 insertion(+), 194 deletions(-) delete mode 100644 technical.manual.md diff --git a/packages/README.md b/packages/README.md index a5b93d321..98938d6c3 100644 --- a/packages/README.md +++ b/packages/README.md @@ -2,4 +2,4 @@ Each folder represents one of the @strudel/* packages [published to npm](https://www.npmjs.com/org/strudel). -To understand how those pieces connect, refer to the [Technical Manual](https://codeberg.org/uzu/strudel/src/branch/main/technical-manual.md) or the individual READMEs. +To understand how those pieces connect, refer to the [Technical Manual](https://codeberg.org/uzu/strudel/wiki/Technical-Manual) or the individual READMEs. diff --git a/technical.manual.md b/technical.manual.md deleted file mode 100644 index 6068b48fe..000000000 --- a/technical.manual.md +++ /dev/null @@ -1,193 +0,0 @@ -This document introduces you to Strudel in a technical sense. If you just want to *use* Strudel, have a look at the [Tutorial](https://strudel.tidalcycles.org/tutorial/). - -## Strudel Packages - -There are different packages for different purposes. They.. - -- split up the code into smaller chunks -- can be selectively used to implement some sort of time based system - -Please refer to the individual README files in the [packages folder](https://codeberg.org/uzu/strudel/src/branch/main/packages) - -## REPL - -The [REPL](https://strudel.tidalcycles.org/) is the place where all packages come together to form a live coding system. It can also be seen as a reference implementation for users of the library. - -More info in the [REPL README](https://codeberg.org/uzu/strudel/src/branch/main/packages/repl/README.md) - -# High Level Overview - - - -## 1. End User Code - -The End User Code is written in JavaScript with added syntax sugar. The [eval package](https://codeberg.org/uzu/strudel/src/branch/main/packages/eval#strudelcycleseval) evaluates the user code -after a transpilation step, which resolves the syntax sugar. If you don't want the syntax sugar, you can omit the eval package and call the native javascript `eval` instead. - -### 🍭 Syntax Sugar - -JavaScript Transpilation = converting valid JavaScript to valid JavaScript: - -```js -"c3 [e3 g3]".fast(2) -``` - -becomes - -```js -mini('c3 [e3 g3]') - .withMiniLocation([1, 0, 0], [1, 11, 11]) // source location - .fast(2); -``` - -- double quoted strings and backtick strings are turned into `mini` calls (single quoted strings are left as is) -- The source location is added by chaining `withMiniLocation`, which enables the real time highlighting later -- (psuedo) variable names that look like notes (like `c4`, `bb2` or `fs3`) are turned into strings -- support for top level await -- operator overloading could be implemented in the future - -This is how it works: - - - -- The user code is parsed with a [shift parser](https://github.com/shapesecurity/shift-parser-js), generating an AST -- The AST is transformed to resolve the syntax sugar -- The AST is used to generate code again (shift-codegen) - -Shift will most likely be replaced with acorn in the future, see https://codeberg.org/uzu/strudel/issues/174 - -### Mini Notation - -Another important part of the user code is the mini notation, which allows to express rhythms in a short manner. - -- the mini notation is [implemented as a PEG grammar](https://codeberg.org/uzu/strudel/src/branch/main/packages/mini/krill.pegjs), living in the [mini package](https://codeberg.org/uzu/strudel/src/branch/main/packages/mini) -- it is based on [krill](https://github.com/Mdashdotdashn/krill) by Mdashdotdashn -- the peg grammar is used to generate a parser with [peggyjs](https://peggyjs.org/) -- the generated parser takes a mini notation string and outputs an AST -- the AST can then be used to construct a pattern using the regular Strudel API - -Here's an example AST: - -```json -{ - "type_": "pattern", - "arguments_": { "alignment": "h" }, - "source_": [ - { - "type_": "element", "source_": "c3", - "location_": { "start": { "offset": 1, "line": 1, "column": 2 }, "end": { "offset": 4, "line": 1, "column": 5 } } - }, - { - "type_": "element", - "location_": { "start": { "offset": 4, "line": 1, "column": 5 }, "end": { "offset": 11, "line": 1, "column": 12 } } - "source_": { - "type_": "pattern", "arguments_": { "alignment": "h" }, - "source_": [ - { - "type_": "element", "source_": "e3", - "location_": { "start": { "offset": 5, "line": 1, "column": 6 }, "end": { "offset": 8, "line": 1, "column": 9 } } - }, - { - "type_": "element", "source_": "g3", - "location_": { "start": { "offset": 8, "line": 1, "column": 9 }, "end": { "offset": 10, "line": 1, "column": 11 } } - } - ] - }, - } - ] -} -``` - -which translates to `seq(c3, seq(e3, g3))` - -## 2. Querying & Scheduling - -When the user code has been evaluated, we hopefully get a Pattern instance, which we can use to query events from. -These events can then be used to trigger side effects in the real world. On that note, Events are mostly called Hap(s) in the codebase, because JS already has a built in `Event` class. - -### Querying - -> Querying = Asking a Pattern for Events within a certain time span - -```js -seq('c3', ['e3', 'g3']) // <--- Pattern - .queryArc(0, 2) // query events within 0 and 2 cycles - .map((hap) => hap.showWhole()); // make readable -``` - -yields - -```js -[ - '0/1 -> 1/2: c3', // cycle 0 - '1/2 -> 3/4: e3', - '3/4 -> 1/1: g3', - '1/1 -> 3/2: c3', // cycle 1 - '3/2 -> 7/4: e3', - '7/4 -> 2/1: g3', -]; -``` - -### 🗓️ Scheduling - -The scheduler will query events repeatedly, creating a possibly endless loop of time slices. -Here is a simplified example of how it works - -```js -let step = 0.5; // query interval in seconds -let tick = 0; // how many intervals have passed -let pattern = seq('c3', ['e3', 'g3']); // pattern from user -setInterval(() => { - const events = pattern.queryArc(tick * step, ++tick * step); - events.forEach((event) => { - console.log(event.showWhole()); - const o = getAudioContext().createOscillator(); - o.frequency.value = getFreq(event.value); - o.start(event.whole.begin); - o.stop(event.whole.begin + event.duration); - o.connect(getAudioContext().destination); - }); -}, step * 1000); // query each "step" seconds -``` - -## 3. Sound Output - -The third and last step is to use the scheduled events to make sound. -Patterns are wrapped with param functions to compose different properties of the sound. - -```js -note("[c2(3,8) [ bb1]]") // sets frequency - .s("") // sound source - .gain(.5) // turn down volume - .cutoff(sine.range(200,1000).slow(4)) // modulated cutoff - .slow(2) - .out().logValues()`, - ]} -/> -``` - -Here is an example Hap value with different properties: - -```js -{ note: 'a4', s: 'sawtooth', gain: 0.5, cutoff: 267 } -``` - - -
- -- Patterns represent just values in time! -- Suitable for any time based output (music, visuals, movement, .. ?) - -### Supported Outputs - -At the time of writing this doc, the following outputs are supported: - -- Web Audio API `.out()` see [/webaudio](https://codeberg.org/uzu/strudel/src/branch/main/packages/webaudio) -- MIDI `.midi()` see [/midi](https://codeberg.org/uzu/strudel/src/branch/main/packages/midi) -- OSC `.osc()` see [/osc](https://codeberg.org/uzu/strudel/src/branch/main/packages/osc) -- Serial `.serial()` see [/serial](https://codeberg.org/uzu/strudel/src/branch/main/packages/serial) -- Tone.js `.tone()` (deprecated?) [/tone](https://codeberg.org/uzu/strudel/src/branch/main/packages/tone) -- WebDirt `.webdirt()` (deprecated?) [/webdirt](https://codeberg.org/uzu/strudel/src/branch/main/packages/webdirt) -- Speech `.speak()` (experimental) part of [/core](https://codeberg.org/uzu/strudel/src/branch/main/packages/core) - -These could change, so make sure to check the [packages folder](https://codeberg.org/uzu/strudel/src/branch/main/packages). From 9738c90b82dbae40e2bf5ad2e477d464d644ccdc Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 14 Jun 2025 16:23:03 +0200 Subject: [PATCH 130/174] even more degithubbing --- packages/core/test/pattern.test.mjs | 2 +- packages/repl/README.md | 2 +- paper/demo-preprocessed.md | 2 +- paper/demo.md | 2 +- paper/iclc2023.html | 6 +- paper/iclc2023.md | 2 +- .../src/components/Footer/AvatarList.astro | 169 ------------------ website/src/components/Header/Search.tsx | 2 +- .../RightSidebar/RightSidebar.astro | 1 - .../src/repl/components/panel/WelcomeTab.jsx | 2 +- 10 files changed, 10 insertions(+), 180 deletions(-) delete mode 100644 website/src/components/Footer/AvatarList.astro diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs index 696f13fef..45a1e2a98 100644 --- a/packages/core/test/pattern.test.mjs +++ b/packages/core/test/pattern.test.mjs @@ -884,7 +884,7 @@ describe('Pattern', () => { ); }); it('Doesnt drop haps in the 9th cycle', () => { - // fixed with https://github.com/tidalcycles/strudel/commit/72eeaf446e3d5e186d63cc0d2276f0723cde017a + // fixed with https://codeberg.org/uzu/strudel/commit/72eeaf446e3d5e186d63cc0d2276f0723cde017a expect(sequence(1, 2, 3).ply(2).early(8).firstCycle().length).toBe(6); }); }); diff --git a/packages/repl/README.md b/packages/repl/README.md index 3db4cc3f7..5b43b5e79 100644 --- a/packages/repl/README.md +++ b/packages/repl/README.md @@ -17,7 +17,7 @@ You can also pin the version like this: ``` This has the advantage that your code will always work, regardless of potential breaking changes in the strudel codebase. -See [releases](https://github.com/tidalcycles/strudel/releases) for the latest versions. +See [releases](https://codeberg.org/uzu/strudel/releases) for the latest versions. ## Use Web Component diff --git a/paper/demo-preprocessed.md b/paper/demo-preprocessed.md index e22823d90..2f0871e7c 100644 --- a/paper/demo-preprocessed.md +++ b/paper/demo-preprocessed.md @@ -201,7 +201,7 @@ interfaces. The Strudel REPL is available at , including an interactive tutorial. The repository is at -, all the code is open source +, all the code is open source under the GPL-3.0 License. # Acknowledgments diff --git a/paper/demo.md b/paper/demo.md index 23fe27754..841fb6064 100644 --- a/paper/demo.md +++ b/paper/demo.md @@ -128,7 +128,7 @@ For the future, it is planned to integrate alternative sound engines such as Gli # Links The Strudel REPL is available at , including an interactive tutorial. -The repository is at , all the code is open source under the GPL-3.0 License. +The repository is at , all the code is open source under the GPL-3.0 License. # Acknowledgments diff --git a/paper/iclc2023.html b/paper/iclc2023.html index a83e075a2..3771f560b 100644 --- a/paper/iclc2023.html +++ b/paper/iclc2023.html @@ -374,7 +374,7 @@ by the output.
REPL control flow
@@ -720,8 +720,8 @@ class="header-section-number">11 Links href="https://strudel.cc" class="uri">https://strudel.cc, including an interactive tutorial. The repository is at https://github.com/tidalcycles/strudel, all the code is +href="https://codeberg.org/uzu/strudel" +class="uri">https://codeberg.org/uzu/strudel, all the code is open source under the AGPL-3.0 License.

12 Acknowledgments

diff --git a/paper/iclc2023.md b/paper/iclc2023.md index 3afb27828..fdc9f0ca4 100644 --- a/paper/iclc2023.md +++ b/paper/iclc2023.md @@ -451,7 +451,7 @@ While Haskell's type system makes it a great language for the ongoing developmen # Links The Strudel REPL is available at , including an interactive tutorial. -The repository is at , all the code is open source under the AGPL-3.0 License. +The repository is at , all the code is open source under the AGPL-3.0 License. # Acknowledgments diff --git a/website/src/components/Footer/AvatarList.astro b/website/src/components/Footer/AvatarList.astro deleted file mode 100644 index 86bbcc875..000000000 --- a/website/src/components/Footer/AvatarList.astro +++ /dev/null @@ -1,169 +0,0 @@ ---- -// fetch all commits for just this page's path -type Props = { - path: string; -}; -const { path } = Astro.props as Props; -const resolvedPath = `website/src/pages${path}.mdx`; -const url = `https://api.github.com/repos/tidalcycles/strudel/commits?path=${resolvedPath}`; -const commitsURL = `https://github.com/tidalcycles/strudel/commits/main/${resolvedPath}`; - -type Commit = { - author: { - id: string; - login: string; - }; -}; - -async function getCommits(url: string) { - try { - const token = import.meta.env.SNOWPACK_PUBLIC_GITHUB_TOKEN ?? 'hello'; - if (!token) { - throw new Error('Cannot find "SNOWPACK_PUBLIC_GITHUB_TOKEN" used for escaping rate-limiting.'); - } - - const auth = `Basic ${Buffer.from(token, 'binary').toString('base64')}`; - - const res = await fetch(url, { - method: 'GET', - headers: { - Authorization: auth, - 'User-Agent': 'astro-docs/1.0', - }, - }); - - const data = await res.json(); - - if (!res.ok) { - throw new Error( - `Request to fetch commits failed. Reason: ${res.statusText} - Message: ${data.message}`, - ); - } - - return data as Commit[]; - } catch (e) { - console.warn(`[error] /src/components/AvatarList.astro - ${(e as any)?.message ?? e}`); - return [] as Commit[]; - } -} - -function removeDups(arr: Commit[]) { - const map = new Map(); - for (let item of arr) { - const author = item.author; - // Deduplicate based on author.id - //map.set(author.id, { login: author.login, id: author.id }); - author && map.set(author.id, { login: author.login, id: author.id }); - } - - return [...map.values()]; -} - -const data = await getCommits(url); -const unique = removeDups(data); -const recentContributors = unique.slice(0, 3); // only show avatars for the 3 most recent contributors -const additionalContributors = unique.length - recentContributors.length; // list the rest of them as # of extra contributors ---- - - -
-
    - { - recentContributors.map((item) => ( -
  • - - {`Contributor - -
  • - )) - } -
- { - additionalContributors > 0 && ( - - {`and ${additionalContributors} additional contributor${ - additionalContributors > 1 ? 's' : '' - }.`} - - ) - } - {unique.length === 0 && Contributors} -
- - diff --git a/website/src/components/Header/Search.tsx b/website/src/components/Header/Search.tsx index c982d80b6..f200dc79f 100644 --- a/website/src/components/Header/Search.tsx +++ b/website/src/components/Header/Search.tsx @@ -82,7 +82,7 @@ export default function Search() { appId={ALGOLIA.appId} apiKey={ALGOLIA.apiKey} getMissingResultsUrl={({ query }) => { - return `https://github.com/tidalcycles/strudel/issues/new?title=Missing doc for ${query}`; + return `https://codeberg.org/uzu/strudel/issues/new?title=Missing%20doc%20for${encodeURIComponent(query)}`; }} transformItems={(items) => { return items.map((item) => { diff --git a/website/src/components/RightSidebar/RightSidebar.astro b/website/src/components/RightSidebar/RightSidebar.astro index cd501b1a2..28c713022 100644 --- a/website/src/components/RightSidebar/RightSidebar.astro +++ b/website/src/components/RightSidebar/RightSidebar.astro @@ -18,5 +18,4 @@ currentPage = currentPage.endsWith('/') ? currentPage.slice(0, -1) : currentPage diff --git a/website/src/repl/components/panel/WelcomeTab.jsx b/website/src/repl/components/panel/WelcomeTab.jsx index b3b167c74..cd5fe030a 100644 --- a/website/src/repl/components/panel/WelcomeTab.jsx +++ b/website/src/repl/components/panel/WelcomeTab.jsx @@ -42,7 +42,7 @@ export function WelcomeTab({ context }) { GNU Affero General Public License . You can find the source code at{' '} - + github . You can also find licensing info{' '} From ee7d7830992ee521dac2d511158c0a0f3c133ed0 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 14 Jun 2025 16:27:54 +0200 Subject: [PATCH 131/174] fix homepage link --- website/src/pages/learn/getting-started.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/pages/learn/getting-started.mdx b/website/src/pages/learn/getting-started.mdx index 2d056d3b4..b3348ccda 100644 --- a/website/src/pages/learn/getting-started.mdx +++ b/website/src/pages/learn/getting-started.mdx @@ -14,7 +14,7 @@ These pages will introduce you to [Strudel](https://strudel.cc/), a web-based [l # What is Strudel? -[Strudel](https://strudel.cc/) is a version of [Tidal Cycles](https://tidalcycles.org) written in [JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript), initiated by [Alex McLean](https://slab.org) and [Felix Roos](https://github.com/felixroos) in 2022. +[Strudel](https://strudel.cc/) is a version of [Tidal Cycles](https://tidalcycles.org) written in [JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript), initiated by [Alex McLean](https://slab.org) and [Felix Roos](https://froos.cc/) in 2022. Tidal Cycles, also known as Tidal, is a language for [algorithmic pattern](https://algorithmicpattern.org), and though it is most commonly used for [making music](https://tidalcycles.org/docs/showcase), it can be used for any kind of pattern making activity, including [weaving](https://www.youtube.com/watch?v=TfEmEsusXjU). Tidal was first implemented as a library written in the [Haskell](https://www.haskell.org/) functional programming language, and by itself it does not make any sound. From 2cb22e94b56bde24a461e6c216381c8c3242de11 Mon Sep 17 00:00:00 2001 From: alex Date: Sat, 14 Jun 2025 23:26:59 +0100 Subject: [PATCH 132/174] move iclc paper under docs/ --- {paper => docs/iclc2023-paper}/Makefile | 0 {paper => docs/iclc2023-paper}/README.md | 0 {paper => docs/iclc2023-paper}/bin/code-filter.py | 0 {paper => docs/iclc2023-paper}/citations.json | 0 {paper => docs/iclc2023-paper}/demo-preprocessed.md | 0 {paper => docs/iclc2023-paper}/demo.md | 0 {paper => docs/iclc2023-paper}/demo.pdf | Bin {paper => docs/iclc2023-paper}/iclc-reviews.md | 0 {paper => docs/iclc2023-paper}/iclc2023.html | 0 {paper => docs/iclc2023-paper}/iclc2023.md | 0 {paper => docs/iclc2023-paper}/iclc2023.pdf | Bin {paper => docs/iclc2023-paper}/iclc2023x.pdf | Bin {paper => docs/iclc2023-paper}/images/cc.png | Bin .../iclc2023-paper}/images/strudel-screenshot.png | Bin .../iclc2023-paper}/images/strudel-screenshot2.png | Bin .../iclc2023-paper}/images/strudelflow.png | Bin {paper => docs/iclc2023-paper}/inconsolata.sty | 0 {paper => docs/iclc2023-paper}/make.sh | 0 {paper => docs/iclc2023-paper}/pandoc/iclc.html | 0 {paper => docs/iclc2023-paper}/pandoc/iclc.latex | 0 {paper => docs/iclc2023-paper}/pandoc/iclc.sty | 0 .../iclc2023-paper}/paper-preprocessed.md | 0 {paper => docs/iclc2023-paper}/paper.md | 0 {paper => docs/iclc2023-paper}/paper.pdf | Bin .../iclc2023-paper}/tex/latex-template.tex | 0 .../iclc2023-paper}/tex/sig-alternate.cls | 0 {paper => docs/iclc2023-paper}/tex/waccopyright.sty | 0 27 files changed, 0 insertions(+), 0 deletions(-) rename {paper => docs/iclc2023-paper}/Makefile (100%) rename {paper => docs/iclc2023-paper}/README.md (100%) rename {paper => docs/iclc2023-paper}/bin/code-filter.py (100%) rename {paper => docs/iclc2023-paper}/citations.json (100%) rename {paper => docs/iclc2023-paper}/demo-preprocessed.md (100%) rename {paper => docs/iclc2023-paper}/demo.md (100%) rename {paper => docs/iclc2023-paper}/demo.pdf (100%) rename {paper => docs/iclc2023-paper}/iclc-reviews.md (100%) rename {paper => docs/iclc2023-paper}/iclc2023.html (100%) rename {paper => docs/iclc2023-paper}/iclc2023.md (100%) rename {paper => docs/iclc2023-paper}/iclc2023.pdf (100%) rename {paper => docs/iclc2023-paper}/iclc2023x.pdf (100%) rename {paper => docs/iclc2023-paper}/images/cc.png (100%) rename {paper => docs/iclc2023-paper}/images/strudel-screenshot.png (100%) rename {paper => docs/iclc2023-paper}/images/strudel-screenshot2.png (100%) rename {paper => docs/iclc2023-paper}/images/strudelflow.png (100%) rename {paper => docs/iclc2023-paper}/inconsolata.sty (100%) rename {paper => docs/iclc2023-paper}/make.sh (100%) rename {paper => docs/iclc2023-paper}/pandoc/iclc.html (100%) rename {paper => docs/iclc2023-paper}/pandoc/iclc.latex (100%) rename {paper => docs/iclc2023-paper}/pandoc/iclc.sty (100%) rename {paper => docs/iclc2023-paper}/paper-preprocessed.md (100%) rename {paper => docs/iclc2023-paper}/paper.md (100%) rename {paper => docs/iclc2023-paper}/paper.pdf (100%) rename {paper => docs/iclc2023-paper}/tex/latex-template.tex (100%) rename {paper => docs/iclc2023-paper}/tex/sig-alternate.cls (100%) rename {paper => docs/iclc2023-paper}/tex/waccopyright.sty (100%) diff --git a/paper/Makefile b/docs/iclc2023-paper/Makefile similarity index 100% rename from paper/Makefile rename to docs/iclc2023-paper/Makefile diff --git a/paper/README.md b/docs/iclc2023-paper/README.md similarity index 100% rename from paper/README.md rename to docs/iclc2023-paper/README.md diff --git a/paper/bin/code-filter.py b/docs/iclc2023-paper/bin/code-filter.py similarity index 100% rename from paper/bin/code-filter.py rename to docs/iclc2023-paper/bin/code-filter.py diff --git a/paper/citations.json b/docs/iclc2023-paper/citations.json similarity index 100% rename from paper/citations.json rename to docs/iclc2023-paper/citations.json diff --git a/paper/demo-preprocessed.md b/docs/iclc2023-paper/demo-preprocessed.md similarity index 100% rename from paper/demo-preprocessed.md rename to docs/iclc2023-paper/demo-preprocessed.md diff --git a/paper/demo.md b/docs/iclc2023-paper/demo.md similarity index 100% rename from paper/demo.md rename to docs/iclc2023-paper/demo.md diff --git a/paper/demo.pdf b/docs/iclc2023-paper/demo.pdf similarity index 100% rename from paper/demo.pdf rename to docs/iclc2023-paper/demo.pdf diff --git a/paper/iclc-reviews.md b/docs/iclc2023-paper/iclc-reviews.md similarity index 100% rename from paper/iclc-reviews.md rename to docs/iclc2023-paper/iclc-reviews.md diff --git a/paper/iclc2023.html b/docs/iclc2023-paper/iclc2023.html similarity index 100% rename from paper/iclc2023.html rename to docs/iclc2023-paper/iclc2023.html diff --git a/paper/iclc2023.md b/docs/iclc2023-paper/iclc2023.md similarity index 100% rename from paper/iclc2023.md rename to docs/iclc2023-paper/iclc2023.md diff --git a/paper/iclc2023.pdf b/docs/iclc2023-paper/iclc2023.pdf similarity index 100% rename from paper/iclc2023.pdf rename to docs/iclc2023-paper/iclc2023.pdf diff --git a/paper/iclc2023x.pdf b/docs/iclc2023-paper/iclc2023x.pdf similarity index 100% rename from paper/iclc2023x.pdf rename to docs/iclc2023-paper/iclc2023x.pdf diff --git a/paper/images/cc.png b/docs/iclc2023-paper/images/cc.png similarity index 100% rename from paper/images/cc.png rename to docs/iclc2023-paper/images/cc.png diff --git a/paper/images/strudel-screenshot.png b/docs/iclc2023-paper/images/strudel-screenshot.png similarity index 100% rename from paper/images/strudel-screenshot.png rename to docs/iclc2023-paper/images/strudel-screenshot.png diff --git a/paper/images/strudel-screenshot2.png b/docs/iclc2023-paper/images/strudel-screenshot2.png similarity index 100% rename from paper/images/strudel-screenshot2.png rename to docs/iclc2023-paper/images/strudel-screenshot2.png diff --git a/paper/images/strudelflow.png b/docs/iclc2023-paper/images/strudelflow.png similarity index 100% rename from paper/images/strudelflow.png rename to docs/iclc2023-paper/images/strudelflow.png diff --git a/paper/inconsolata.sty b/docs/iclc2023-paper/inconsolata.sty similarity index 100% rename from paper/inconsolata.sty rename to docs/iclc2023-paper/inconsolata.sty diff --git a/paper/make.sh b/docs/iclc2023-paper/make.sh similarity index 100% rename from paper/make.sh rename to docs/iclc2023-paper/make.sh diff --git a/paper/pandoc/iclc.html b/docs/iclc2023-paper/pandoc/iclc.html similarity index 100% rename from paper/pandoc/iclc.html rename to docs/iclc2023-paper/pandoc/iclc.html diff --git a/paper/pandoc/iclc.latex b/docs/iclc2023-paper/pandoc/iclc.latex similarity index 100% rename from paper/pandoc/iclc.latex rename to docs/iclc2023-paper/pandoc/iclc.latex diff --git a/paper/pandoc/iclc.sty b/docs/iclc2023-paper/pandoc/iclc.sty similarity index 100% rename from paper/pandoc/iclc.sty rename to docs/iclc2023-paper/pandoc/iclc.sty diff --git a/paper/paper-preprocessed.md b/docs/iclc2023-paper/paper-preprocessed.md similarity index 100% rename from paper/paper-preprocessed.md rename to docs/iclc2023-paper/paper-preprocessed.md diff --git a/paper/paper.md b/docs/iclc2023-paper/paper.md similarity index 100% rename from paper/paper.md rename to docs/iclc2023-paper/paper.md diff --git a/paper/paper.pdf b/docs/iclc2023-paper/paper.pdf similarity index 100% rename from paper/paper.pdf rename to docs/iclc2023-paper/paper.pdf diff --git a/paper/tex/latex-template.tex b/docs/iclc2023-paper/tex/latex-template.tex similarity index 100% rename from paper/tex/latex-template.tex rename to docs/iclc2023-paper/tex/latex-template.tex diff --git a/paper/tex/sig-alternate.cls b/docs/iclc2023-paper/tex/sig-alternate.cls similarity index 100% rename from paper/tex/sig-alternate.cls rename to docs/iclc2023-paper/tex/sig-alternate.cls diff --git a/paper/tex/waccopyright.sty b/docs/iclc2023-paper/tex/waccopyright.sty similarity index 100% rename from paper/tex/waccopyright.sty rename to docs/iclc2023-paper/tex/waccopyright.sty From cc4cad05b2f7fce4f50155a4799a67ea35bd5b2e Mon Sep 17 00:00:00 2001 From: alex Date: Sat, 14 Jun 2025 23:33:52 +0100 Subject: [PATCH 133/174] add the technical manual again --- docs/technical-manual/index.md | 197 ++++++++++++++++++++++++++ docs/technical-manual/shiftflow.png | Bin 0 -> 90584 bytes docs/technical-manual/strudelflow.png | Bin 0 -> 84696 bytes docs/technical-manual/waa-nodes.png | Bin 0 -> 50450 bytes 4 files changed, 197 insertions(+) create mode 100644 docs/technical-manual/index.md create mode 100644 docs/technical-manual/shiftflow.png create mode 100644 docs/technical-manual/strudelflow.png create mode 100644 docs/technical-manual/waa-nodes.png diff --git a/docs/technical-manual/index.md b/docs/technical-manual/index.md new file mode 100644 index 000000000..16c9a5533 --- /dev/null +++ b/docs/technical-manual/index.md @@ -0,0 +1,197 @@ +This document introduces you to Strudel in a technical sense. + +It is rather out of date, but there might still be useful info below. + +If you just want to *use* Strudel, have a look at the [Tutorial](https://strudel.tidalcycles.org/tutorial/). + +## Strudel Packages + +There are different packages for different purposes. They.. + +- split up the code into smaller chunks +- can be selectively used to implement some sort of time based system + +Please refer to the individual README files in the [packages folder](https://codeberg.org/uzu/strudel/src/branch/main/packages) + +## REPL + +The [REPL](https://strudel.tidalcycles.org/) is the place where all packages come together to form a live coding system. It can also be seen as a reference implementation for users of the library. + +More info in the [REPL README](https://codeberg.org/uzu/strudel/src/branch/main/packages/repl/README.md) + +# High Level Overview + + + +## 1. End User Code + +The End User Code is written in JavaScript with added syntax sugar. The [eval package](https://github.com/tidalcycles/strudel/tree/main/packages/eval#strudelcycleseval) evaluates the user code +after a transpilation step, which resolves the syntax sugar. If you don't want the syntax sugar, you can omit the eval package and call the native javascript `eval` instead. + +### 🍭 Syntax Sugar + +JavaScript Transpilation = converting valid JavaScript to valid JavaScript: + +```js +"c3 [e3 g3]".fast(2) +``` + +becomes + +```js +mini('c3 [e3 g3]') + .withMiniLocation([1, 0, 0], [1, 11, 11]) // source location + .fast(2); +``` + +- double quoted strings and backtick strings are turned into `mini` calls (single quoted strings are left as is) +- The source location is added by chaining `withMiniLocation`, which enables the real time highlighting later +- (psuedo) variable names that look like notes (like `c4`, `bb2` or `fs3`) are turned into strings +- support for top level await +- operator overloading could be implemented in the future + +This is how it works: + + + +- The user code is parsed with a [shift parser](https://github.com/shapesecurity/shift-parser-js), generating an AST +- The AST is transformed to resolve the syntax sugar +- The AST is used to generate code again (shift-codegen) + +Shift will most likely be replaced with acorn in the future, see https://github.com/tidalcycles/strudel/issues/174 + +### Mini Notation + +Another important part of the user code is the mini notation, which allows to express rhythms in a short manner. + +- the mini notation is [implemented as a PEG grammar](https://github.com/tidalcycles/strudel/blob/main/packages/mini/krill.pegjs), living in the [mini package](https://github.com/tidalcycles/strudel/tree/main/packages/mini) +- it is based on [krill](https://github.com/Mdashdotdashn/krill) by Mdashdotdashn +- the peg grammar is used to generate a parser with [peggyjs](https://peggyjs.org/) +- the generated parser takes a mini notation string and outputs an AST +- the AST can then be used to construct a pattern using the regular Strudel API + +Here's an example AST: + +```json +{ + "type_": "pattern", + "arguments_": { "alignment": "h" }, + "source_": [ + { + "type_": "element", "source_": "c3", + "location_": { "start": { "offset": 1, "line": 1, "column": 2 }, "end": { "offset": 4, "line": 1, "column": 5 } } + }, + { + "type_": "element", + "location_": { "start": { "offset": 4, "line": 1, "column": 5 }, "end": { "offset": 11, "line": 1, "column": 12 } } + "source_": { + "type_": "pattern", "arguments_": { "alignment": "h" }, + "source_": [ + { + "type_": "element", "source_": "e3", + "location_": { "start": { "offset": 5, "line": 1, "column": 6 }, "end": { "offset": 8, "line": 1, "column": 9 } } + }, + { + "type_": "element", "source_": "g3", + "location_": { "start": { "offset": 8, "line": 1, "column": 9 }, "end": { "offset": 10, "line": 1, "column": 11 } } + } + ] + }, + } + ] +} +``` + +which translates to `seq(c3, seq(e3, g3))` + +## 2. Querying & Scheduling + +When the user code has been evaluated, we hopefully get a Pattern instance, which we can use to query events from. +These events can then be used to trigger side effects in the real world. On that note, Events are mostly called Hap(s) in the codebase, because JS already has a built in `Event` class. + +### Querying + +> Querying = Asking a Pattern for Events within a certain time span + +```js +seq('c3', ['e3', 'g3']) // <--- Pattern + .queryArc(0, 2) // query events within 0 and 2 cycles + .map((hap) => hap.showWhole()); // make readable +``` + +yields + +```js +[ + '0/1 -> 1/2: c3', // cycle 0 + '1/2 -> 3/4: e3', + '3/4 -> 1/1: g3', + '1/1 -> 3/2: c3', // cycle 1 + '3/2 -> 7/4: e3', + '7/4 -> 2/1: g3', +]; +``` + +### 🗓️ Scheduling + +The scheduler will query events repeatedly, creating a possibly endless loop of time slices. +Here is a simplified example of how it works + +```js +let step = 0.5; // query interval in seconds +let tick = 0; // how many intervals have passed +let pattern = seq('c3', ['e3', 'g3']); // pattern from user +setInterval(() => { + const events = pattern.queryArc(tick * step, ++tick * step); + events.forEach((event) => { + console.log(event.showWhole()); + const o = getAudioContext().createOscillator(); + o.frequency.value = getFreq(event.value); + o.start(event.whole.begin); + o.stop(event.whole.begin + event.duration); + o.connect(getAudioContext().destination); + }); +}, step * 1000); // query each "step" seconds +``` + +## 3. Sound Output + +The third and last step is to use the scheduled events to make sound. +Patterns are wrapped with param functions to compose different properties of the sound. + +```js +note("[c2(3,8) [ bb1]]") // sets frequency + .s("") // sound source + .gain(.5) // turn down volume + .cutoff(sine.range(200,1000).slow(4)) // modulated cutoff + .slow(2) + .out().logValues()`, + ]} +/> +``` + +Here is an example Hap value with different properties: + +```js +{ note: 'a4', s: 'sawtooth', gain: 0.5, cutoff: 267 } +``` + + +
+ +- Patterns represent just values in time! +- Suitable for any time based output (music, visuals, movement, .. ?) + +### Supported Outputs + +At the time of writing this doc, the following outputs are supported: + +- Web Audio API `.out()` see [/webaudio](https://github.com/tidalcycles/strudel/tree/main/packages/webaudio) +- MIDI `.midi()` see [/midi](https://github.com/tidalcycles/strudel/tree/main/packages/midi) +- OSC `.osc()` see [/osc](https://github.com/tidalcycles/strudel/tree/main/packages/osc) +- Serial `.serial()` see [/serial](https://github.com/tidalcycles/strudel/tree/main/packages/serial) +- Tone.js `.tone()` (deprecated?) [/tone](https://github.com/tidalcycles/strudel/tree/main/packages/tone) +- WebDirt `.webdirt()` (deprecated?) [/webdirt](https://github.com/tidalcycles/strudel/tree/main/packages/webdirt) +- Speech `.speak()` (experimental) part of [/core](https://github.com/tidalcycles/strudel/tree/main/packages/core) + +These could change, so make sure to check the [packages folder](https://github.com/tidalcycles/strudel/tree/main/packages). diff --git a/docs/technical-manual/shiftflow.png b/docs/technical-manual/shiftflow.png new file mode 100644 index 0000000000000000000000000000000000000000..8d91658dcb1d5db29f5c8009dcfadc0d9200b19f GIT binary patch literal 90584 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 *sd!B@`9#?etzVvn33C3{X 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 literal 0 HcmV?d00001 diff --git a/docs/technical-manual/strudelflow.png b/docs/technical-manual/strudelflow.png new file mode 100644 index 0000000000000000000000000000000000000000..72cdf494bdb28e181df263387e412fe95698c795 GIT binary patch literal 84696 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 z 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%0Dl0)fVK4U6MUl5WZb0& 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 DG-OT) literal 0 HcmV?d00001 diff --git a/docs/technical-manual/waa-nodes.png b/docs/technical-manual/waa-nodes.png new file mode 100644 index 0000000000000000000000000000000000000000..cece1aa91ad50241c23557b6d2acda4787dbbb5d GIT binary patch literal 50450 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 literal 0 HcmV?d00001 From bebbe30b50fcb0925be20437a6cc6acca3d69f3a Mon Sep 17 00:00:00 2001 From: alex Date: Sun, 15 Jun 2025 08:03:23 +0100 Subject: [PATCH 134/174] codeformat --- docs/iclc2023-paper/iclc2023.html | 1696 ++++++++++++++------------ docs/iclc2023-paper/pandoc/iclc.html | 135 +- 2 files changed, 975 insertions(+), 856 deletions(-) diff --git a/docs/iclc2023-paper/iclc2023.html b/docs/iclc2023-paper/iclc2023.html index a83e075a2..add08377f 100644 --- a/docs/iclc2023-paper/iclc2023.html +++ b/docs/iclc2023-paper/iclc2023.html @@ -1,458 +1,598 @@ - - - - - - Strudel: live coding patterns on the Web - - - - - -

+ + + + + + Strudel: live coding patterns on the Web + + + + + + -

Abstract

-
-

This paper introduces Strudel, which brings the TidalCycles approach -to live coding algorithmic patterns to native JavaScript and the web. We -begin by giving a little background of the first year of development, -before sharing some detail about its implementation and examples of use. -We go on to outline the wide range of synthesis and other outputs -available in Strudel, including WebAudio, MIDI, OSC (for SuperDirt), -WebSerial and CSound, and introduce Strudel’s REPL live editor, -including its built-in visualisations. We then compare Strudel with -Tidal, the trade-offs involved between JavaScript and Haskell, and the -unique capabilities offered by Strudel for aligning patterns.

-
+

Abstract

+
+

+ This paper introduces Strudel, which brings the TidalCycles approach to live coding algorithmic patterns to + native JavaScript and the web. We begin by giving a little background of the first year of development, before + sharing some detail about its implementation and examples of use. We go on to outline the wide range of + synthesis and other outputs available in Strudel, including WebAudio, MIDI, OSC (for SuperDirt), WebSerial and + CSound, and introduce Strudel’s REPL live editor, including its built-in visualisations. We then compare Strudel + with Tidal, the trade-offs involved between JavaScript and Haskell, and the unique capabilities offered by + Strudel for aligning patterns. +

+
-

1 Introduction

-

In the following paper, we introduce Strudel, an alternative -implementation of the TidalCycles (or ‘Tidal’ for short) live coding -system, using the JavaScript programming language. Strudel is an attempt -to make live coding more accessible, by creating a system that runs -entirely in the browser, while opening Tidal’s approach to algorithmic -patterns (Mclean 2020) up to -modern audio/visual web technologies. The Strudel REPL is a live code -editor dedicated to manipulating patterns while they play, with builtin -visual feedback. While Strudel is written in JavaScript, the API is -optimized for simplicity and readability by applying code -transformations on the syntax tree level, allowing language operations -that would otherwise be impossible. The application supports multiple -ways to output sound, including Tone.js, Web Audio Nodes, OSC (Open -Sound Control) messages, Web Serial, Web MIDI and Csound. The project is -split into multiple packages, allowing granular reuse in other -applications. Apart from TidalCycles, Strudel draws inspiration from -many prior existing projects like TidalVortex (McLean et al. 2022), -Gibber (Roberts and Kuchera-morin -2012), Estuary (Ogborn et al. -2017), Hydra (Jack [2022] 2022), Ocarina (Solomon -[2021] 2022) and Feedforward (McLean 2020). This paper -expands the Strudel Demo paper for the Web Audio Conference 2022 (Roos and McLean -2022).

-

The first tentative commit to the Strudel project was on 22nd January -2022 by Alex McLean, with the core representation implemented over the -following few days. Although this was his first attempt at a -JavaScript-based application, by 27th January, Alex had managed to -upload the initial version to the ‘npm’ javascript package database, -sharing with the wider community for comment. By 4th February, Felix -Roos had discovered Strudel and contributed a ‘REPL’ user interface to -it, and then contributed a scheduler the next day, so that Strudel could -already make sound. At this point, Alex and Felix shared ownership to -the repository, and the project has since proved to be a productive -confluence of Felix’s own work into music representation and -visualisation, with Alex’s experience with making Tidal. Felix has since -become the primary contributor to Strudel, with Alex continuing to jump -between developing both Strudel and Tidal. Aspects of Strudel’s -development have therefore fed back into TidalCycles, and both systems -have maintained a shared conceptual underpinning. We plan to continue -working towards feature parity between these systems, although within -the syntactical trade-offs and library ecosystems of JavaScript and -Haskell, some divergence is inevitable and healthy.

-

Over the first year of its life, Strudel is now a fully-fledged live -coding environment, porting Tidal’s core represention of patterns, -pattern transformations, and mininotation for polymetric sequences, -combined with a wealth of features for synthesising and visualising -those patterns.

-

2 From Tidal to Strudel and -back

-

As mentioned above, the original Tidal is implemented as a domain -specific language (DSL) embedded in the Haskell pure functional -programming language, and takes advantage of Haskell’s terse syntax and -advanced, ‘strong’ type system. JavaScript on the other hand, is a -multi-paradigm programming language, with a dynamic type system. Because -Tidal leans heavily on many of Haskell’s more unique features, it was -not always clear that it could meaningfully be ported to a -multi-paradigm scripting language. However, this possibility was already -demonstrated with an earlier port to Python [TidalVortex; McLean et al. -(2022)], and we have now successfully implemented Tidal’s pure -functional representation of patterns in Strudel, including partial -application, currying, and the functor, applicative and monadic -structures that underlie Tidal’s expressive pattern transformations. The -result is a terse and highly composable system, where everything is -either a pattern, or a function for combining and manipulating patterns, -offering a rich creative ground for exploration.

-

This development process has been far from a one-way port, however. -The process of porting Tidal’s concepts has also opened up new -possibilities, some just from revisiting every design decision, and some -from the particular affordances and constraints offered by JavaScript. -This has lead to new features (and indeed bugfixes) that have found -their way back to Tidal where appropriate, and ongoing work that we will -return to in the conclusion of this paper.

-

3 Representing Patterns

-

Patterns are the essence of Tidal. Its patterns are abstract entities -that represent flows of time as functions, adapting a technique called -pure functional reactive programming. Taking a time span as its input, a -Pattern can output a set of events that happen within that time span. It -depends on the structure of the Pattern how the events are located in -time. From now on, this process of generating events from a time span -will be called querying. Example:

-
const pattern = sequence(c3, [e3, g3])
+    

1 Introduction

+

+ In the following paper, we introduce Strudel, an alternative implementation of the TidalCycles (or + ‘Tidal’ for short) live coding system, using the JavaScript programming language. Strudel is an attempt to make + live coding more accessible, by creating a system that runs entirely in the browser, while opening Tidal’s + approach to algorithmic patterns + (Mclean 2020) up to modern audio/visual + web technologies. The Strudel REPL is a live code editor dedicated to manipulating patterns while they play, with + builtin visual feedback. While Strudel is written in JavaScript, the API is optimized for simplicity and + readability by applying code transformations on the syntax tree level, allowing language operations that would + otherwise be impossible. The application supports multiple ways to output sound, including Tone.js, Web Audio + Nodes, OSC (Open Sound Control) messages, Web Serial, Web MIDI and Csound. The project is split into multiple + packages, allowing granular reuse in other applications. Apart from TidalCycles, Strudel draws inspiration from + many prior existing projects like TidalVortex + (McLean et al. 2022), Gibber + (Roberts and Kuchera-morin 2012), Estuary + (Ogborn et al. 2017), Hydra + (Jack [2022] 2022), Ocarina + (Solomon [2021] 2022) and Feedforward + (McLean 2020). This paper expands the Strudel + Demo paper for the Web Audio Conference 2022 + (Roos and McLean 2022). +

+

+ The first tentative commit to the Strudel project was on 22nd January 2022 by Alex McLean, with the core + representation implemented over the following few days. Although this was his first attempt at a JavaScript-based + application, by 27th January, Alex had managed to upload the initial version to the ‘npm’ javascript package + database, sharing with the wider community for comment. By 4th February, Felix Roos had discovered Strudel and + contributed a ‘REPL’ user interface to it, and then contributed a scheduler the next day, so that Strudel could + already make sound. At this point, Alex and Felix shared ownership to the repository, and the project has since + proved to be a productive confluence of Felix’s own work into music representation and visualisation, with Alex’s + experience with making Tidal. Felix has since become the primary contributor to Strudel, with Alex continuing to + jump between developing both Strudel and Tidal. Aspects of Strudel’s development have therefore fed back into + TidalCycles, and both systems have maintained a shared conceptual underpinning. We plan to continue working + towards feature parity between these systems, although within the syntactical trade-offs and library ecosystems of + JavaScript and Haskell, some divergence is inevitable and healthy. +

+

+ Over the first year of its life, Strudel is now a fully-fledged live coding environment, porting Tidal’s core + represention of patterns, pattern transformations, and mininotation for polymetric sequences, combined with a + wealth of features for synthesising and visualising those patterns. +

+

+ 2 From Tidal to Strudel and back +

+

+ As mentioned above, the original Tidal is implemented as a domain specific language (DSL) embedded in the Haskell + pure functional programming language, and takes advantage of Haskell’s terse syntax and advanced, ‘strong’ type + system. JavaScript on the other hand, is a multi-paradigm programming language, with a dynamic type system. + Because Tidal leans heavily on many of Haskell’s more unique features, it was not always clear that it could + meaningfully be ported to a multi-paradigm scripting language. However, this possibility was already demonstrated + with an earlier port to Python [TidalVortex; + McLean et al. (2022)], and we have now + successfully implemented Tidal’s pure functional representation of patterns in Strudel, including partial + application, currying, and the functor, applicative and monadic structures that underlie Tidal’s expressive + pattern transformations. The result is a terse and highly composable system, where everything is either a pattern, + or a function for combining and manipulating patterns, offering a rich creative ground for exploration. +

+

+ This development process has been far from a one-way port, however. The process of porting Tidal’s concepts has + also opened up new possibilities, some just from revisiting every design decision, and some from the particular + affordances and constraints offered by JavaScript. This has lead to new features (and indeed bugfixes) that have + found their way back to Tidal where appropriate, and ongoing work that we will return to in the conclusion of this + paper. +

+

+ 3 Representing Patterns +

+

+ Patterns are the essence of Tidal. Its patterns are abstract entities that represent flows of time as functions, + adapting a technique called pure functional reactive programming. Taking a time span as its input, a Pattern can + output a set of events that happen within that time span. It depends on the structure of the Pattern how the + events are located in time. From now on, this process of generating events from a time span will be called + querying. Example: +

+
+
const pattern = sequence(c3, [e3, g3])
 const events = pattern.queryArc(0, 1)
-console.log(events.map(e => e.show()))
-

In this example, we create a pattern using the sequence -function and query it for the time span from -0 to 1. Those numbers represent units of time -called cycles. The length of one cycle depends on the -tempo, which defaults to one cycle per second. The resulting events -are:

-
[{ value: 'c3', begin: 0, end: 1/2 },
+console.log(events.map(e => e.show()))
+
+

+ In this example, we create a pattern using the sequence function and query it for + the time span from 0 to 1. Those numbers represent units of time called + cycles. The length of one cycle depends on the tempo, which defaults to one cycle per second. The + resulting events are: +

+
+
[{ value: 'c3', begin: 0, end: 1/2 },
 { value: 'e3', begin: 1/2, end: 3/4 },
-{ value: 'g3', begin: 3/4, end: 1 }]
-

Each event has a value, a begin time and an end time, where time is -represented as a fraction. In the above case, the events are placed in -sequential order, where c3 takes the first half, and e3 and g3 together -take the second half. This temporal placement is the result of the -sequence function, which divides its arguments equally over -one cycle. If an argument is an array, the same rule applies to that -part of the cycle. In the example, e3 and g3 are divided equally over -the second half of the whole cycle.

-

The above examples do not represent how Strudel is used in practice. -In the live coding editor, the user only has to type in the pattern -itself, the querying will be handled by the scheduler. The scheduler -will repeatedly query the pattern for events, which are then scheduled -as sound synthesis or other event triggers. Also, the above event data -structure has been simplified for readability.

-
- - -
-

4 Making Patterns

-

In practice, the end-user live coder will not deal with constructing -patterns directly, but will rather build patterns using Strudel’s -extensive combinator library to create, combine and transform -patterns.

-

The live coder will rarely use the sequence function as -seen above, as sequencing is implicit in many functions. For example in -the following, the note function constructs a pattern of -notes, sequencing its arguments in the same manner as the previous -example.

-
note(c3, [e3, g3])
-

Perhaps more often, they will use the mini-notation for even terser -notation of rhythmic sequences: [^This last example is also valid Tidal -code, albeit the parenthesis is not required in its Haskell syntax in -this case. Tidal does not support passing sequences as lists directly to -the note function, however.].

-
note("c3 [e3 g3]")
-

Such sequences are often treated only a starting point for -manipulation, where they then undergo pattern transformations such as -repetition, symmetry, interference/combination or randomisation, -potentially at multiple timescales. Because Strudel patterns are -represented as pure functions of time rather than as data structures, -very long and complex generative results can be represented and -manipulated without having to store the resulting sequences in -memory.

-

5 Pattern Example

-

The following example showcases how patterns can be utilized to -create musical complexity from simple parts, using repetition and -interference:

-
"<0 2 [4 6](3,4,1) 3>"
+{ value: 'g3', begin: 3/4, end: 1 }]
+
+

+ Each event has a value, a begin time and an end time, where time is represented as a fraction. In the above case, + the events are placed in sequential order, where c3 takes the first half, and e3 and g3 together take the second + half. This temporal placement is the result of the sequence function, which divides its arguments + equally over one cycle. If an argument is an array, the same rule applies to that part of the cycle. In the + example, e3 and g3 are divided equally over the second half of the whole cycle. +

+

+ The above examples do not represent how Strudel is used in practice. In the live coding editor, the user only has + to type in the pattern itself, the querying will be handled by the scheduler. The scheduler will repeatedly query + the pattern for events, which are then scheduled as sound synthesis or other event triggers. Also, the above event + data structure has been simplified for readability. +

+
+ Screenshot of the Strudel ‘REPL’ live coding editor, including piano-roll visualisation. + +
+

4 Making Patterns

+

+ In practice, the end-user live coder will not deal with constructing patterns directly, but will rather build + patterns using Strudel’s extensive combinator library to create, combine and transform patterns. +

+

+ The live coder will rarely use the sequence function as seen above, as sequencing is implicit in many + functions. For example in the following, the note function constructs a pattern of notes, sequencing + its arguments in the same manner as the previous example. +

+
+
note(c3, [e3, g3])
+
+

+ Perhaps more often, they will use the mini-notation for even terser notation of rhythmic sequences: [^This last + example is also valid Tidal code, albeit the parenthesis is not required in its Haskell syntax in this case. Tidal + does not support passing sequences as lists directly to the note function, however.]. +

+
+
note("c3 [e3 g3]")
+
+

+ Such sequences are often treated only a starting point for manipulation, where they then undergo pattern + transformations such as repetition, symmetry, interference/combination or randomisation, potentially at multiple + timescales. Because Strudel patterns are represented as pure functions of time rather than as data structures, + very long and complex generative results can be represented and manipulated without having to store the resulting + sequences in memory. +

+

5 Pattern Example

+

+ The following example showcases how patterns can be utilized to create musical complexity from simple parts, using + repetition and interference: +

+
+
"<0 2 [4 6](3,4,1) 3>"
 .off(1/4, add(2))
 .off(1/2, add(6))
 .scale('D minor')
 .legato(.25)
 .note().s("sawtooth square")
-.delay(.8).delaytime(.125)
-

The pattern starts with a rhythm of numbers in mini notation, which -are later interpreted inside the scale of D minor. The first line could -also be expressed without mini notation:

-
cat(0, 2, [4, 6].euclid(3, 4, 1), 3)
-

These numbers then undergo various pattern transformations. Here is a -short description of all the functions used:

-
    -
  • cat: play elements sequentially, where each lasts one -cycle
  • -
  • brackets: elements inside brackets are divided equally -over the time of their parent
  • -
  • .euclid(p, s, o): place p pulses evenly over s steps, -with offset o (Toussaint -2005)
  • -
  • .off(n, f): layers a pattern on top of itself, with the -new layer offset by n cycles, and with function f applied
  • -
  • .legato(n): multiply the duration of all events in a -pattern by a factor of n
  • -
  • .echo(t, n, v): copy each event t times, with n cycles -in between each copy, decreasing velocity by v
  • -
  • .note(): interpretes values as notes
  • -
  • .s(name): play back each event with the given -sound
  • -
  • .delay(wet): add delay
  • -
  • .delaytime(t): set delay time
  • -
-

Much of the above will be familiar to Tidal users.

- -

6 Ways to make Sound (and other -events)

-

To generate sound, Strudel supports bindings for different -outputs:

-
    -
  • Tone.js (deprecated)
  • -
  • Web Audio API
  • -
  • WebDirt, a js recreation of Tidal’s Dirt sample engine -(deprecated)
  • -
  • OSC via osc-js, compatible with superdirt
  • -
  • Csound via the Csound WebAssembly build
  • -
  • MIDI via WebMIDI
  • -
  • Serial via WebSerial
  • -
-

At first, we used Tone.js as sound output, but it proved to be -limited for the use case of Strudel, where each individual event could -potentially have a completely different audio graph. While the Web Audio -API takes a fire-and-forget approach, creating a lot of Tone.js -instruments and effects causes performance issues quickly. For that -reason, we chose to search for alternatives.

-

Strudel’s new default output uses the Web Audio API to create a new -audio graph for each event. It currently supports basic oscillators, -sample playback, various effects and an experimental support for -soundfonts.

-

WebDirt (Ogborn [2016] 2022) was -created as part of the Estuary Live Coding System (Ogborn et al. -2017), and proved to be a solid choice for handling samples in -Strudel as well. We are however focused on working more directly with -the Web Audio API to be able to integrate new features more tightly.

-

Using the OSC protocol via Strudel’s provided Node.js-based OSC proxy -server, it is possible to send network messages to trigger events. This -is mainly used to render sound using SuperDirt (SuperDirt [2015] 2022), -which is the well-developed Supercollider-based synthesis framework that -Tidal live coders generally use as standard.

-

Recently, the experimental integration of Csound proved to bring a -new dimension of sound design capabilities to Strudel. Thanks to the -WebAssembly distribution of this classic system (Yi, Lazzarini, and Costello -2018), Csound ‘orchestra’ synthesisers can be embedded in and -then patterned with Strudel code.

-

MIDI output can also be used to send MIDI messages to either external -instruments or to other programs on the same device. Unlike OSC, Strudel -is able to send MIDI directly without requiring additional proxy -software, but only from web browsers that support it (at the time of -writing, this means Chromium-based browsers).

-

Finally, Strudel supports Serial output, for example to trigger -events via microcontrollers. This has already been explored for robot -choreography by Kate Sicchio and Alex McLean, via a performance -presented at the International Conference on Live Interfaces 2022.

-

7 The Strudel REPL

-

While Strudel can be used as a library in any JavaScript codebase, -its main, reference user interface is the Strudel REPL[^REPL stands for -read, evaluate, print/play, loop. It is friendly jargon for an -interactive programming interface from computing heritage, usually for a -commandline interface but also applied to live coding editors.], which -is a browser-based live coding environment. This live code editor is -dedicated to manipulating Strudel patterns while they play. The REPL -features built-in visual feedback, which highlights which elements in -the patterned (mini-notation) sequences are influencing the event that -is currently being played. This feedback is designed to support both -learning and live use of Strudel.

-

Besides a UI for playback control and meta information, the main part -of the REPL interface is the code editor powered by CodeMirror. In it, -the user can edit and evaluate pattern code live, using one of the -available synthesis outputs to create music and/or sound art. The -control flow of the REPL follows 3 basic steps:

-
    -
  1. The user writes and updates code. Each update transpiles and -evaluates it to create a Pattern instance
  2. -
  3. While the REPL is running, the Scheduler queries the -active Pattern by a regular interval, generating -Events (also known as Haps in Strudel) for the -next time span.
  4. -
  5. For each scheduling tick, all generated Events are -triggered by calling their onTrigger method, which is set -by the output.
  6. -
-
-REPL control flow - -
-

7.1 User Code

-

To create a Pattern from the user code, two steps are -needed:

-
    -
  1. Transpile the JS input code to make it functional
  2. -
  3. Evaluate the transpiled code
  4. -
-

7.1.1 Transpilation & -Evaluation

-

In the JavaScript world, using transpilation is a common practise to -be able to use language features that are not supported by the base -language. Tools like babel will transpile code that -contains unsupported language features into a version of the code -without those features.

-

In the same tradition, Strudel can add a transpilation step to -simplify the user code in the context of live coding. For example, the -Strudel REPL lets the user create mini notation patterns using just -double quoted strings, while single quoted strings remain what they -are:

-
"c3 [e3 g3]*2"
-

is transpiled to:

-
mini("c3 [e3 g3]*2").withMiniLocation([1,0,0],[1,14,14])
-

Here, the string is wrapped in mini, which will create a -pattern from a mini notation string. Additionally, the -withMiniLocation method passes the original source code -location of the string to the pattern, which enables highlighting active -events.

-

Other convenient features like pseudo variables, operator overloading -and top level await are possible with transpilation.

-

After the transpilation, the code is ready to be evaluated into a -Pattern.

-

Behind the scenes, the user code string is parsed with -acorn, turning it into an Abstract Syntax Tree (AST). The -AST allows changing the structure of the code before generating the -transpiled version using escodegen.

-

7.1.2 Mini Notation

-

While the transpilation allows JavaScript to express Patterns in a -less verbose way, it is still preferable to use the Mini Notation as a -more compact way to express rhythm. Strudel aims to provide the same -Mini Notation features and syntax as used in Tidal.

-

The Mini Notation parser is implemented using peggy, -which allows generating performant parsers for Domain Specific Languages -(DSLs) using a concise grammar notation. The generated parser turns the -Mini Notation string into an AST which is used to call the respective -Strudel functions with the given structure. For example, -"c3 [e3 g3]*2" will result in the following calls:

-
seq(
+.delay(.8).delaytime(.125)
+
+

+ The pattern starts with a rhythm of numbers in mini notation, which are later interpreted inside the scale of D + minor. The first line could also be expressed without mini notation: +

+
+
cat(0, 2, [4, 6].euclid(3, 4, 1), 3)
+
+

+ These numbers then undergo various pattern transformations. Here is a short description of all the functions used: +

+
    +
  • cat: play elements sequentially, where each lasts one cycle
  • +
  • brackets: elements inside brackets are divided equally over the time of their parent
  • +
  • + .euclid(p, s, o): place p pulses evenly over s steps, with offset o + (Toussaint 2005) +
  • +
  • + .off(n, f): layers a pattern on top of itself, with the new layer offset by n cycles, and with + function f applied +
  • +
  • .legato(n): multiply the duration of all events in a pattern by a factor of n
  • +
  • + .echo(t, n, v): copy each event t times, with n cycles in between each copy, decreasing velocity by + v +
  • +
  • .note(): interpretes values as notes
  • +
  • .s(name): play back each event with the given sound
  • +
  • .delay(wet): add delay
  • +
  • .delaytime(t): set delay time
  • +
+

Much of the above will be familiar to Tidal users.

+ +

+ 6 Ways to make Sound (and other events) +

+

To generate sound, Strudel supports bindings for different outputs:

+
    +
  • Tone.js (deprecated)
  • +
  • Web Audio API
  • +
  • WebDirt, a js recreation of Tidal’s Dirt sample engine (deprecated)
  • +
  • OSC via osc-js, compatible with superdirt
  • +
  • Csound via the Csound WebAssembly build
  • +
  • MIDI via WebMIDI
  • +
  • Serial via WebSerial
  • +
+

+ At first, we used Tone.js as sound output, but it proved to be limited for the use case of Strudel, where each + individual event could potentially have a completely different audio graph. While the Web Audio API takes a + fire-and-forget approach, creating a lot of Tone.js instruments and effects causes performance issues + quickly. For that reason, we chose to search for alternatives. +

+

+ Strudel’s new default output uses the Web Audio API to create a new audio graph for each event. It currently + supports basic oscillators, sample playback, various effects and an experimental support for soundfonts. +

+

+ WebDirt (Ogborn [2016] 2022) was created as part + of the Estuary Live Coding System + (Ogborn et al. 2017), and + proved to be a solid choice for handling samples in Strudel as well. We are however focused on working more + directly with the Web Audio API to be able to integrate new features more tightly. +

+

+ Using the OSC protocol via Strudel’s provided Node.js-based OSC proxy server, it is possible to send network + messages to trigger events. This is mainly used to render sound using SuperDirt + (SuperDirt [2015] 2022), which is the + well-developed Supercollider-based synthesis framework that Tidal live coders generally use as standard. +

+

+ Recently, the experimental integration of Csound proved to bring a new dimension of sound design capabilities to + Strudel. Thanks to the WebAssembly distribution of this classic system + (Yi, Lazzarini, and Costello 2018), Csound + ‘orchestra’ synthesisers can be embedded in and then patterned with Strudel code. +

+

+ MIDI output can also be used to send MIDI messages to either external instruments or to other programs on the same + device. Unlike OSC, Strudel is able to send MIDI directly without requiring additional proxy software, but only + from web browsers that support it (at the time of writing, this means Chromium-based browsers). +

+

+ Finally, Strudel supports Serial output, for example to trigger events via microcontrollers. This has already been + explored for robot choreography by Kate Sicchio and Alex McLean, via a performance presented at the International + Conference on Live Interfaces 2022. +

+

7 The Strudel REPL

+

+ While Strudel can be used as a library in any JavaScript codebase, its main, reference user interface is the + Strudel REPL[^REPL stands for read, evaluate, print/play, loop. It is friendly jargon for an interactive + programming interface from computing heritage, usually for a commandline interface but also applied to live coding + editors.], which is a browser-based live coding environment. This live code editor is dedicated to manipulating + Strudel patterns while they play. The REPL features built-in visual feedback, which highlights which elements in + the patterned (mini-notation) sequences are influencing the event that is currently being played. This feedback is + designed to support both learning and live use of Strudel. +

+

+ Besides a UI for playback control and meta information, the main part of the REPL interface is the code editor + powered by CodeMirror. In it, the user can edit and evaluate pattern code live, using one of the available + synthesis outputs to create music and/or sound art. The control flow of the REPL follows 3 basic steps: +

+
    +
  1. + The user writes and updates code. Each update transpiles and evaluates it to create a + Pattern instance +
  2. +
  3. + While the REPL is running, the Scheduler queries the active Pattern by a regular + interval, generating Events (also known as Haps in Strudel) for the next time span. +
  4. +
  5. + For each scheduling tick, all generated Events are triggered by calling their + onTrigger method, which is set by the output. +
  6. +
+
+ REPL control flow + +
+

7.1 User Code

+

To create a Pattern from the user code, two steps are needed:

+
    +
  1. Transpile the JS input code to make it functional
  2. +
  3. Evaluate the transpiled code
  4. +
+

+ 7.1.1 Transpilation & Evaluation +

+

+ In the JavaScript world, using transpilation is a common practise to be able to use language features that are not + supported by the base language. Tools like babel will transpile code that contains unsupported + language features into a version of the code without those features. +

+

+ In the same tradition, Strudel can add a transpilation step to simplify the user code in the context of live + coding. For example, the Strudel REPL lets the user create mini notation patterns using just double quoted + strings, while single quoted strings remain what they are: +

+
+
"c3 [e3 g3]*2"
+
+

is transpiled to:

+
+
mini("c3 [e3 g3]*2").withMiniLocation([1,0,0],[1,14,14])
+
+

+ Here, the string is wrapped in mini, which will create a pattern from a mini notation string. + Additionally, the withMiniLocation method passes the original source code location of the string to + the pattern, which enables highlighting active events. +

+

+ Other convenient features like pseudo variables, operator overloading and top level await are possible with + transpilation. +

+

After the transpilation, the code is ready to be evaluated into a Pattern.

+

+ Behind the scenes, the user code string is parsed with acorn, turning it into an Abstract Syntax Tree + (AST). The AST allows changing the structure of the code before generating the transpiled version using + escodegen. +

+

7.1.2 Mini Notation

+

+ While the transpilation allows JavaScript to express Patterns in a less verbose way, it is still preferable to use + the Mini Notation as a more compact way to express rhythm. Strudel aims to provide the same Mini Notation features + and syntax as used in Tidal. +

+

+ The Mini Notation parser is implemented using peggy, which allows generating performant parsers for + Domain Specific Languages (DSLs) using a concise grammar notation. The generated parser turns the Mini Notation + string into an AST which is used to call the respective Strudel functions with the given structure. For example, + "c3 [e3 g3]*2" will result in the following calls: +

+
+
seq(
   reify('c3').withLocation([1,1,1], [1,4,4]),
   seq(
     reify('e3').withLocation([1,5,5], [1,8,8]),
     reify('g3').withLocation([1,8,8], [1,10,10]),
   ).fast(2)
-)
-

7.1.3 Highlighting Locations

-

As seen in the examples above, both the JS and the Mini Notation -parser add source code locations using withMiniLocation and -withLocation methods. While the JS parser adds locations -relative to the user code as a whole, the Mini Notation adds locations -relative to the position of the mini notation string. The absolute -location of elements within Mini Notation can be calculated by simply -adding both locations together. This absolute location can be used to -highlight active events in real time.

-

7.2 Scheduling Events

-

After an instance of Pattern is obtained from the user -code, it is used by the scheduler to get queried for events. Once -started, the scheduler runs at a fixed interval to query active pattern -for events withing the current interval’s time span. A simplified -implementation looks like this:

-
let pattern = seq('c3', ['e3', 'g3']); // pattern from user
+)
+
+

+ 7.1.3 Highlighting Locations +

+

+ As seen in the examples above, both the JS and the Mini Notation parser add source code locations using + withMiniLocation and withLocation methods. While the JS parser adds locations relative + to the user code as a whole, the Mini Notation adds locations relative to the position of the mini notation + string. The absolute location of elements within Mini Notation can be calculated by simply adding both locations + together. This absolute location can be used to highlight active events in real time. +

+

7.2 Scheduling Events

+

+ After an instance of Pattern is obtained from the user code, it is used by the scheduler to get + queried for events. Once started, the scheduler runs at a fixed interval to query active pattern for events + withing the current interval’s time span. A simplified implementation looks like this: +

+
+
let pattern = seq('c3', ['e3', 'g3']); // pattern from user
 let interval = 0.5; // query interval in seconds
 let time = 0; // beginning of current time span
 let minLatency = .1; // min time before a hap should trigger
@@ -463,70 +603,80 @@ class="sourceCode js">    const deadline = hap.whole.begin - time + minLatency;
     onTrigger(hap, deadline, duration);
   });
-}, interval * 1000); // query each "interval" seconds
-

Note that the above code is simplified for illustrative purposes. The -actual implementation has to work around imprecise callbacks of -setInterval. More about the implementation details can be -read in this -blog post.

-

The fact that Pattern.queryArc is a pure function that -maps a time span to a set of events allows us to choose any interval we -like without changing the resulting output. It also means that when the -pattern is changed from outside, the next scheduling callback will work -with the new pattern, keeping its clock running.

-

The latency between the time the pattern is evaluated and the change -is heard is between minLatency and -interval + minLatency, in our example between 100ms and -600ms. In Strudel, the current query interval is 50ms with a minLatency -of 100ms, meaning the latency is between 50ms and 150ms.

-

7.3 Output

-

The last step is to trigger each event in the chosen output. This is -where the given time and value of each event is used to generate audio -or any other form of time based output. The default output of the -Strudel REPL is the WebAudio output. To understand what an output does, -we first have to understand what control parameters are.

-

7.3.1 Control Parameters

-

To be able to manipulate multiple aspects of sound in parallel, so -called control parameters are used to shape the value of each event. -Example:

-
note("c3 e3").cutoff(1000).s('sawtooth')
+}, interval * 1000); // query each "interval" seconds
+
+

+ Note that the above code is simplified for illustrative purposes. The actual implementation has to work around + imprecise callbacks of setInterval. More about the implementation details can be read in + this blog post. +

+

+ The fact that Pattern.queryArc is a pure function that maps a time span to a set of events allows us + to choose any interval we like without changing the resulting output. It also means that when the pattern is + changed from outside, the next scheduling callback will work with the new pattern, keeping its clock running. +

+

+ The latency between the time the pattern is evaluated and the change is heard is between + minLatency and interval + minLatency, in our example between 100ms and 600ms. In + Strudel, the current query interval is 50ms with a minLatency of 100ms, meaning the latency is between 50ms and + 150ms. +

+

7.3 Output

+

+ The last step is to trigger each event in the chosen output. This is where the given time and value of each event + is used to generate audio or any other form of time based output. The default output of the Strudel REPL is the + WebAudio output. To understand what an output does, we first have to understand what control parameters are. +

+

+ 7.3.1 Control Parameters +

+

+ To be able to manipulate multiple aspects of sound in parallel, so called control parameters are used to shape the + value of each event. Example: +

+
+
note("c3 e3").cutoff(1000).s('sawtooth')
   .queryArc(0, 1).map(hap => hap.value)
 /* [
   { note: 'c3', cutoff: 1000, s: 'sawtooth' }
   { note: 'e3', cutoff: 1000, s: 'sawtooth' }
-] */
-

Here, the control parameter functions note, -cutoff and s are used, where each controls a -different property in the value object. Each control parameter function -accepts a primitive value, a list of values to be sequenced into a -Pattern, or a Pattern. In the example, -note gets a Pattern from a Mini Notation -expression (double quoted), while cutoff and s -are given a Number and a (single quoted) -String respectively.

-

Strudel comes with a large default set of control parameter functions -that are based on the ones used by Tidal and SuperDirt, focusing on -music and audio terminology. It is however possible to create custom -control paramters for any purpose:

-
const { x, y } = createParams('x', 'y')
-x(sine.range(0, 200)).y(cosine.range(0,200))
-

This example creates the custom control parameters x and -y which are then used to form a pattern that descibes the -coordinates of a circle.

-

7.3.2 Outputs

-

Now that we know how the value of an event is manipulated using -control parameters, we can look at how outputs can use that value to -generate anything. The scheduler above was calling the -onTrigger function which is used to implement the output. A -very simple version of the web audio output could look like this:

-
function onTrigger(hap, deadline, duration) {
+] */
+
+

+ Here, the control parameter functions note, cutoff and s are used, where + each controls a different property in the value object. Each control parameter function accepts a primitive value, + a list of values to be sequenced into a Pattern, or a Pattern. In the example, + note gets a Pattern from a Mini Notation expression (double quoted), while + cutoff and s are given a Number and a (single quoted) + String respectively. +

+

+ Strudel comes with a large default set of control parameter functions that are based on the ones used by Tidal and + SuperDirt, focusing on music and audio terminology. It is however possible to create custom control paramters for + any purpose: +

+
+
const { x, y } = createParams('x', 'y')
+x(sine.range(0, 200)).y(cosine.range(0,200))
+
+

+ This example creates the custom control parameters x and y which are then used to form a + pattern that descibes the coordinates of a circle. +

+

7.3.2 Outputs

+

+ Now that we know how the value of an event is manipulated using control parameters, we can look at how outputs can + use that value to generate anything. The scheduler above was calling the onTrigger function which is + used to implement the output. A very simple version of the web audio output could look like this: +

+
+
function onTrigger(hap, deadline, duration) {
   const { note } = hap.value;
   const time = getAudioContext().currentTime + deadline;
   const o = getAudioContext().createOscillator();
@@ -534,285 +684,271 @@ class="sourceCode js">  o.start(time);
   o.stop(time + event.duration);
   o.connect(getAudioContext().destination);
-}
-

The above example will create an OscillatorNode for each -event, where the frequency is controlled by the note param. -In essence, this is how the WebAudio API output of Strudel works, only -with many more parameters to control synths, samples and effects.

-

8 Pattern alignment and -combination

-

One core aspect of Strudel, inherited from Tidal, is the flexible way -that patterns can be combined, irrespective of their structure. Its -declarative approach means a live coder does not have to think about the -details of how this is done, only what is to be -done.

-

As a simple example, consider two number patterns -"0 [1 2] 3", and "10 20". The first has three -contiguous steps of equal lengths, with the second step broken down into -two substeps, giving four events in total. There are a very large number -of ways in which the structure of these two patterns could be combined, -but the default method in both Strudel and Tidal is to line up the -cycles of the two patterns, and then take events from the first pattern -and match them with those in the second pattern. Therefore, the -following two lines are equivalent:

-
"0 [1 2] 3".add("10 20")
-"10 [11 22] 23"
-

Where the events only partially overlap, they are treated as -fragments of the event in the first pattern. This is a little difficult -to conceptualise, but lets start by comparing the two patterns in the -following example:

-
"0 1 2".add("10 20")
-"10 [11 21] 20"
-

They are similar to the previous example in that the number -1 is split in two, with its two halves added to -10 and 20 respectively. However, the -11 ‘remembers’ that it is a fragment of that original -1 event, and so is treated as having a duration of a third -of a cycle, despite only being active for a sixth of a cycle. Likewise, -the 21 is also a fragment of that original 1 -event, but a fragment of its second half. Because the start of its event -is missing, it wouldn’t actually trigger a sound (unless it underwent -further pattern transformations/combinations).

-

In practice, the effect of this default, implicit method for -combining two patterns is that the second pattern is added in -to the first one, and indeed this can be made explicit:

-
"0 1 2".add.in("10 20")
-

This makes way for other ways to align the pattern, and several are -already defined, in particular:

-
    -
  • in - as explained above, aligns cycles, and applies -values from the pattern on the right in to the pattern on the -left.
  • -
  • out - as with in, but values are applied -out of the pattern on the left (i.e. in to the one on -the right).
  • -
  • mix - structures from both patterns are combined, so -that the new events are not fragments but are created at intersections -of events from both sides.
  • -
  • squeeze - cycles from the pattern on the right are -squeezed into events on the left. So that -e.g. "0 1 2".add.squeeze("10 20") is equivalent to -"[10 20] [11 21] [12 22]".
  • -
  • squeezeout - as with squeeze, but cycles -from the left are squeezed into events on the right. So, -"0 1 2".add.squeezeout("10 20") is equivalent to -[10 11 12] [20 21 22].
  • -
  • trig is similar to squeezeout in that -cycles from the right are aligned with events on the left. However those -cycles are not ‘squeezed’, rather they are truncated to fit the event. -So "0 1 2 3 4 5 6 7".add.trig("10 [20 30]") would be -equivalent to 10 11 12 13 20 21 30 31. In effect, events on -the right ‘trigger’ cycles on the left.
  • -
  • trigzero is similar to trig, but the -pattern is ‘triggered’ from its very first cycle, rather than from the -current cycle. trig and trigzero therefore -only give different results where the leftmost pattern differs from one -cycle to the next.
  • -
-

We will save going deeper into the background, design and -practicalities of these alignment functions for future publications. -However in the next section, we take them as a case study for looking at -the different design affordances offered by Haskell to Tidal, and -JavaScript to Strudel.

-

9 Comparing Strudel and Haskell in -use

-

Unlike Haskell, JavaScript lacks the ability to define custom infix -operators, or change the meaning of existing ones. So the above Strudel -example of "0 1 2".add.out("10 20") is equivalent to the -Tidal expression "0 1 2" +| "10 20", where the vertical bar -in the operator +| stands for out (where -a |+ b would be equivalent of -a.add.in(b)).

-

From this we can already see that Tidal tends towards brevity through -mixing infix operators with functions, and Strudel tends towards -spelling out operations which are joined together with the -. operator. This then is the design trade-off of Tidal’s -tersity, versus Strudel’s simplicity.

-

To demonstrate this, consider the following Tidal pattern:

-
iter 4 $ every 3 (||+ n "10 20") $ (n "0 1 3") # s "triangle" # crush 4
-

This can be directly translated to the Strudel equivalent:

-
iter(4, every(3, add.squeeze("10 20"), n("0 1 3").s("triangle").crush(4)))
-

Although for a more canonical Strudel expression, we would reorder it -as:

-
n("0 1 3").every(3, add.squeeze("10 20")).iter(4).s("triangle").crush(4)
-

The Strudel example uses the . method call operator for -all operations and combinations, whereas the Tidal example has -# for the default method for combining patterns and uses -infix operators for other methods. The lack of parenthesis in the Tidal -example is partly due to the way that arguments are applied to Haskell’s -functions, and partly due to the use of the $ operator as -an alternative way to establish precedence and control the order of -evaluation.

-

Considering the above, we argue that the Haskell syntax is a little -cleaner, but that the Strudel syntax is easier to learn. Our informal -observation is that while Haskell’s dollar $ operator is -very useful in making code easier to work with, it is one of the most -difficult aspects of Tidal use for beginners to learn. On the other -hand, the deeper levels of parenthesis in Strudel code can be difficult -to keep track of, especially while coding under pressure of live musical -performance. However this difficulty can be largely be mitigated by -reordering expressions, and further mitigated by supporting editor -features.

-

With Strudel, we have little choice but to embrace the affordances -and constraints offered by JavaScript, and while designing a -domain-specific language entirely based on method calls is a challenge, -through creative adoption of functional programming techniques like -partial application, we are so far very happy with the results. Tidal’s -functional reactive approach to pattern-making has in general translated -well to JavaScript, and opportunities and constraints have overall -traded off to create a very approachable and useable live coding -environment.

-

9.1 The trade-off of flexible -typing

-

We have identified one problem with porting Tidal to JavaScript where -we have missed Haskell’s strict typing and type inference. In both Tidal -and Strudel, time is rational, where any point in time is represented as -the ratio of two integers. This allows representation of musical ratios -such that are impossible to represent accurately using the more common -floating point numbers. However while libraries are available that -support rational numbers in JavaScript, the lack of strict typing means -that it is easy to implement pattern methods where computationally -expensive conversion from floating point to rational numbers are -performed late, and therefore often enough to overload the CPUs, due to -the large number of iterative calculations required to estimate a ratio -for a given floating point number. To mitigate this problem, we might -consider moving to TypeScript in the future.

-

10 Future Outlook

-

The project is still young, with many features on the horizon. As -general guiding principles, Strudel aims to be

-
    -
  1. accessible
  2. -
  3. consistent with Tidal’s approach to pattern
  4. -
  5. modular and extensible
  6. -
-

While Haskell’s type system makes it a great language for the ongoing -development of Tidal’s inner representation of pattern, JavaScript’s -vibrant ecosystem, flexibility and accessibility makes it a great host -for more ad-hoc experiments, including interface design. For the future, -it is planned to integrate additional alternative sound engines such as -Glicol (Lan -[2020] 2022) and Faust (Faust - Programming -Language for Audio Applications and Plugins [2016] 2022). -Strudel is already approaching feature parity with Tidal, but there are -more Tidal functions to be ported, and work to be done to improve -compatibility with Tidal’s mininotation. Tidal version 2.0 is under -development, which brings a new representation for sequences to its -patterns, which will then be brought to Strudel. Besides sound, other -ways to render events are being explored, such as graphical, and -choreographic output. We are also looking into alternative ways of -editing patterns, including multi-user editing for network music, -parsing a novel syntax to escape the constraints of javascript, and -developing hardware/e-textile interfaces. In summary, there is a lot of -fun ahead.

-

11 Links

-

The Strudel REPL is available at https://strudel.cc, including an -interactive tutorial. The repository is at https://github.com/tidalcycles/strudel, all the code is -open source under the AGPL-3.0 License.

-

12 Acknowledgments

-

Thanks to the Strudel and wider Tidal, live coding, WebAudio and -free/open source software communities for inspiration and support. Alex -McLean’s work on this project is supported by a UKRI Future Leaders -Fellowship [grant number MR/V025260/1].

-

References

-
-
-Faust - Programming Language for Audio Applications and -Plugins. (2016) 2022. C++. GRAME. https://github.com/grame-cncm/faust. -
-
-Jack, Olivia. (2022) 2022. Hydra. https://github.com/ojack/hydra. -
-
-Lan, Qichao. (2020) 2022. Chaosprint/Glicol. Rust. https://github.com/chaosprint/glicol. -
-
-Mclean, Alex. 2020. “Algorithmic Pattern.” In -Proceedings of the International Conference on New Interfaces for -Musical Expression, 265--270. Birmingham, UK. https://zenodo.org/record/4813352. -
-
-McLean, Alex. 2020. “Feedforward.” In Proceedings of -New Interfaces for Musical Expression. Birmingham. https://zenodo.org/record/6353969. -
-
-McLean, Alex, Raphaël Forment, Sylvain Le Beux, and Damián Silvani. -2022. “TidalVortex Zero.” In Proceedings of the 7th -International Conference on Live Coding. Limerick, Ireland: Zenodo. -https://doi.org/10.5281/zenodo.6456380. -
-
-Ogborn, David. (2016) 2022. Dktr0/WebDirt. JavaScript. https://github.com/dktr0/WebDirt. -
-
-Ogborn, David, Jamie Beverley, Luis Navarro del Angel, Eldad Tsabary, -and Alex McLean. 2017. “Estuary: Browser-Based Collaborative -Projectional Live Coding of Musical Patterns.” In Proceedings -of the International Conference on Live Coding, 11. Morelia. -
-
-Roberts, Charles, and Joann Kuchera-morin. 2012. “Gibber: Live -Coding Audio in the Browser.” In In Proceedings of the 2012 -International Computer Music Conference. -
-
-Roos, Felix, and Alex McLean. 2022. “Strudel: Algorithmic Patterns -for the Web.” In. Zenodo. https://doi.org/10.5281/zenodo.6768844. -
-
-Solomon, Mike. (2021) 2022. Purescript-Ocarina. PureScript. https://github.com/mikesol/purescript-ocarina. -
-
-SuperDirt. (2015) 2022. SuperCollider. musikinformatik. https://github.com/musikinformatik/SuperDirt. -
-
-Toussaint, Godfried. 2005. “The Euclidean Algorithm Generates -Traditional Musical Rhythms.” In In Proceedings of BRIDGES: -Mathematical Connections in Art, Music and Science, 47–56. http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.62.231. -
-
-Yi, Steven, Victor Lazzarini, and Edward Costello. 2018. -“WebAssembly AudioWorklet Csound.” In. Berlin, Germany. https://mural.maynoothuniversity.ie/16018/. -
-
- +}
+
+

+ The above example will create an OscillatorNode for each event, where the frequency is controlled by + the note param. In essence, this is how the WebAudio API output of Strudel works, only with many more + parameters to control synths, samples and effects. +

+

+ 8 Pattern alignment and combination +

+

+ One core aspect of Strudel, inherited from Tidal, is the flexible way that patterns can be combined, irrespective + of their structure. Its declarative approach means a live coder does not have to think about the details of + how this is done, only what is to be done. +

+

+ As a simple example, consider two number patterns "0 [1 2] 3", and "10 20". The first + has three contiguous steps of equal lengths, with the second step broken down into two substeps, giving four + events in total. There are a very large number of ways in which the structure of these two patterns could be + combined, but the default method in both Strudel and Tidal is to line up the cycles of the two patterns, and then + take events from the first pattern and match them with those in the second pattern. Therefore, the following two + lines are equivalent: +

+
+
"0 [1 2] 3".add("10 20")
+"10 [11 22] 23"
+
+

+ Where the events only partially overlap, they are treated as fragments of the event in the first pattern. This is + a little difficult to conceptualise, but lets start by comparing the two patterns in the following example: +

+
+
"0 1 2".add("10 20")
+"10 [11 21] 20"
+
+

+ They are similar to the previous example in that the number 1 is split in two, with its two halves + added to 10 and 20 respectively. However, the 11 ‘remembers’ that it is a + fragment of that original 1 event, and so is treated as having a duration of a third of a cycle, + despite only being active for a sixth of a cycle. Likewise, the 21 is also a fragment of that + original 1 event, but a fragment of its second half. Because the start of its event is missing, it + wouldn’t actually trigger a sound (unless it underwent further pattern transformations/combinations). +

+

+ In practice, the effect of this default, implicit method for combining two patterns is that the second pattern is + added in to the first one, and indeed this can be made explicit: +

+
+
"0 1 2".add.in("10 20")
+
+

This makes way for other ways to align the pattern, and several are already defined, in particular:

+
    +
  • + in - as explained above, aligns cycles, and applies values from the pattern on the right + in to the pattern on the left. +
  • +
  • + out - as with in, but values are applied out of the pattern on the left + (i.e. in to the one on the right). +
  • +
  • + mix - structures from both patterns are combined, so that the new events are not fragments but are + created at intersections of events from both sides. +
  • +
  • + squeeze - cycles from the pattern on the right are squeezed into events on the left. So that + e.g. "0 1 2".add.squeeze("10 20") is equivalent to "[10 20] [11 21] [12 22]". +
  • +
  • + squeezeout - as with squeeze, but cycles from the left are squeezed into events on the + right. So, "0 1 2".add.squeezeout("10 20") is equivalent to [10 11 12] [20 21 22]. +
  • +
  • + trig is similar to squeezeout in that cycles from the right are aligned with events on + the left. However those cycles are not ‘squeezed’, rather they are truncated to fit the event. So + "0 1 2 3 4 5 6 7".add.trig("10 [20 30]") would be equivalent to + 10 11 12 13 20 21 30 31. In effect, events on the right ‘trigger’ cycles on the left. +
  • +
  • + trigzero is similar to trig, but the pattern is ‘triggered’ from its very first cycle, + rather than from the current cycle. trig and trigzero therefore only give different + results where the leftmost pattern differs from one cycle to the next. +
  • +
+

+ We will save going deeper into the background, design and practicalities of these alignment functions for future + publications. However in the next section, we take them as a case study for looking at the different design + affordances offered by Haskell to Tidal, and JavaScript to Strudel. +

+

+ 9 Comparing Strudel and Haskell in use +

+

+ Unlike Haskell, JavaScript lacks the ability to define custom infix operators, or change the meaning of existing + ones. So the above Strudel example of "0 1 2".add.out("10 20") is equivalent to the Tidal expression + "0 1 2" +| "10 20", where the vertical bar in the operator +| stands for + out (where a |+ b would be equivalent of a.add.in(b)). +

+

+ From this we can already see that Tidal tends towards brevity through mixing infix operators with functions, and + Strudel tends towards spelling out operations which are joined together with the . operator. This + then is the design trade-off of Tidal’s tersity, versus Strudel’s simplicity. +

+

To demonstrate this, consider the following Tidal pattern:

+
iter 4 $ every 3 (||+ n "10 20") $ (n "0 1 3") # s "triangle" # crush 4
+

This can be directly translated to the Strudel equivalent:

+
+
iter(4, every(3, add.squeeze("10 20"), n("0 1 3").s("triangle").crush(4)))
+
+

Although for a more canonical Strudel expression, we would reorder it as:

+
+
n("0 1 3").every(3, add.squeeze("10 20")).iter(4).s("triangle").crush(4)
+
+

+ The Strudel example uses the . method call operator for all operations and combinations, whereas the + Tidal example has # for the default method for combining patterns and uses infix operators for other + methods. The lack of parenthesis in the Tidal example is partly due to the way that arguments are applied to + Haskell’s functions, and partly due to the use of the $ operator as an alternative way to establish + precedence and control the order of evaluation. +

+

+ Considering the above, we argue that the Haskell syntax is a little cleaner, but that the Strudel syntax is easier + to learn. Our informal observation is that while Haskell’s dollar $ operator is very useful in making + code easier to work with, it is one of the most difficult aspects of Tidal use for beginners to learn. On the + other hand, the deeper levels of parenthesis in Strudel code can be difficult to keep track of, especially while + coding under pressure of live musical performance. However this difficulty can be largely be mitigated by + reordering expressions, and further mitigated by supporting editor features. +

+

+ With Strudel, we have little choice but to embrace the affordances and constraints offered by JavaScript, and + while designing a domain-specific language entirely based on method calls is a challenge, through creative + adoption of functional programming techniques like partial application, we are so far very happy with the results. + Tidal’s functional reactive approach to pattern-making has in general translated well to JavaScript, and + opportunities and constraints have overall traded off to create a very approachable and useable live coding + environment. +

+

+ 9.1 The trade-off of flexible typing +

+

+ We have identified one problem with porting Tidal to JavaScript where we have missed Haskell’s strict typing and + type inference. In both Tidal and Strudel, time is rational, where any point in time is represented as the ratio + of two integers. This allows representation of musical ratios such that are impossible to represent accurately + using the more common floating point numbers. However while libraries are available that support rational numbers + in JavaScript, the lack of strict typing means that it is easy to implement pattern methods where computationally + expensive conversion from floating point to rational numbers are performed late, and therefore often enough to + overload the CPUs, due to the large number of iterative calculations required to estimate a ratio for a given + floating point number. To mitigate this problem, we might consider moving to TypeScript in the future. +

+

10 Future Outlook

+

+ The project is still young, with many features on the horizon. As general guiding principles, Strudel aims to be +

+
    +
  1. accessible
  2. +
  3. consistent with Tidal’s approach to pattern
  4. +
  5. modular and extensible
  6. +
+

+ While Haskell’s type system makes it a great language for the ongoing development of Tidal’s inner representation + of pattern, JavaScript’s vibrant ecosystem, flexibility and accessibility makes it a great host for more ad-hoc + experiments, including interface design. For the future, it is planned to integrate additional alternative sound + engines such as Glicol (Lan [2020] 2022) and + Faust + (Faust - Programming Language for Audio Applications and Plugins [2016] 2022). Strudel is already approaching feature parity with Tidal, but there are more Tidal functions to be ported, and + work to be done to improve compatibility with Tidal’s mininotation. Tidal version 2.0 is under development, which + brings a new representation for sequences to its patterns, which will then be brought to Strudel. Besides sound, + other ways to render events are being explored, such as graphical, and choreographic output. We are also looking + into alternative ways of editing patterns, including multi-user editing for network music, parsing a novel syntax + to escape the constraints of javascript, and developing hardware/e-textile interfaces. In summary, there is a lot + of fun ahead. +

+

11 Links

+

+ The Strudel REPL is available at https://strudel.cc, including an + interactive tutorial. The repository is at + https://github.com/tidalcycles/strudel, all the + code is open source under the AGPL-3.0 License. +

+

12 Acknowledgments

+

+ Thanks to the Strudel and wider Tidal, live coding, WebAudio and free/open source software communities for + inspiration and support. Alex McLean’s work on this project is supported by a UKRI Future Leaders Fellowship + [grant number MR/V025260/1]. +

+

References

+
+
+ Faust - Programming Language for Audio Applications and Plugins. (2016) 2022. C++. GRAME. + https://github.com/grame-cncm/faust. +
+
+ Jack, Olivia. (2022) 2022. Hydra. + https://github.com/ojack/hydra. +
+
+ Lan, Qichao. (2020) 2022. Chaosprint/Glicol. Rust. + https://github.com/chaosprint/glicol. +
+
+ Mclean, Alex. 2020. “Algorithmic Pattern.” In + Proceedings of the International Conference on New Interfaces for Musical Expression, 265--270. + Birmingham, UK. https://zenodo.org/record/4813352. +
+
+ McLean, Alex. 2020. “Feedforward.” In + Proceedings of New Interfaces for Musical Expression. Birmingham. + https://zenodo.org/record/6353969. +
+
+ McLean, Alex, Raphaël Forment, Sylvain Le Beux, and Damián Silvani. 2022. “TidalVortex Zero.” In + Proceedings of the 7th International Conference on Live Coding. Limerick, Ireland: Zenodo. + https://doi.org/10.5281/zenodo.6456380. +
+
+ Ogborn, David. (2016) 2022. Dktr0/WebDirt. JavaScript. + https://github.com/dktr0/WebDirt. +
+
+ Ogborn, David, Jamie Beverley, Luis Navarro del Angel, Eldad Tsabary, and Alex McLean. 2017. + “Estuary: Browser-Based Collaborative Projectional Live Coding of Musical Patterns.” In + Proceedings of the International Conference on Live Coding, 11. Morelia. +
+
+ Roberts, Charles, and Joann Kuchera-morin. 2012. “Gibber: Live Coding Audio in the Browser.” In + In Proceedings of the 2012 International Computer Music Conference. +
+
+ Roos, Felix, and Alex McLean. 2022. “Strudel: Algorithmic Patterns for the Web.” In. Zenodo. + https://doi.org/10.5281/zenodo.6768844. +
+
+ Solomon, Mike. (2021) 2022. Purescript-Ocarina. PureScript. + https://github.com/mikesol/purescript-ocarina. +
+
+ SuperDirt. (2015) 2022. SuperCollider. musikinformatik. + https://github.com/musikinformatik/SuperDirt. +
+
+ Toussaint, Godfried. 2005. “The Euclidean Algorithm Generates Traditional Musical Rhythms.” In + In Proceedings of BRIDGES: Mathematical Connections in Art, Music and Science, 47–56. + http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.62.231. +
+
+ Yi, Steven, Victor Lazzarini, and Edward Costello. 2018. + “WebAssembly AudioWorklet Csound.” In. Berlin, Germany. + https://mural.maynoothuniversity.ie/16018/. +
+
+ diff --git a/docs/iclc2023-paper/pandoc/iclc.html b/docs/iclc2023-paper/pandoc/iclc.html index 90406b16e..c47d6595c 100755 --- a/docs/iclc2023-paper/pandoc/iclc.html +++ b/docs/iclc2023-paper/pandoc/iclc.html @@ -1,80 +1,63 @@ -$if(false)$ - -This is a pandoc template and should not be edited. - -$endif$ +$if(false)$ This is a pandoc template and should not be edited. $endif$ - - - - - -$for(author-meta)$ - -$endfor$ -$if(date-meta)$ - -$endif$ - $if(title-prefix)$$title-prefix$ - $endif$$pagetitle$ - -$if(quotes)$ - -$endif$ -$if(highlighting-css)$ - -$endif$ - -$for(css)$ - -$endfor$ -$if(math)$ - $math$ -$endif$ -$for(header-includes)$ - $header-includes$ -$endfor$ - - -$for(include-before)$ -$include-before$ -$endfor$ -$if(title)$ -
-

$title$

-$if(subtitle)$ -

$subtitle$

-$endif$ -
    -$for(author)$ -
  • $author$
  • -$endfor$ -
-$if(date)$ -

$date$

-$endif$ -
-$endif$ -$if(toc)$ -
-$toc$ -
-$endif$ + + + + + + $for(author-meta)$ + + $endfor$ $if(date-meta)$ + + $endif$ + $if(title-prefix)$$title-prefix$ - $endif$$pagetitle$ + + $if(quotes)$ + + $endif$ $if(highlighting-css)$ + + $endif$ + + $for(css)$ + + $endfor$ $if(math)$ $math$ $endif$ $for(header-includes)$ $header-includes$ $endfor$ + + + $for(include-before)$ $include-before$ $endfor$ $if(title)$ +
+

$title$

+ $if(subtitle)$ +

$subtitle$

+ $endif$ +
    + $for(author)$ +
  • $author$
  • + $endfor$ +
+ $if(date)$ +

$date$

+ $endif$ +
+ $endif$ $if(toc)$ +
$toc$
+ $endif$ -

Abstract

-
-$if(abstract)$ -$abstract$ -$else$ -Please provide an abstract in the metadata block at the top of your -markdown document. Refer to template.txt for details. -$endif$ -
+

Abstract

+
+ $if(abstract)$ $abstract$ $else$ Please provide an abstract in the metadata block at the top of your markdown + document. Refer to template.txt for details. $endif$ +
-$body$ -$for(include-after)$ -$include-after$ -$endfor$ - + $body$ $for(include-after)$ $include-after$ $endfor$ + From e52017c0d8605a82c4475de82a9ce9e169ee0319 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 15 Jun 2025 10:12:17 +0200 Subject: [PATCH 135/174] fix: reference tab critical fail --- website/src/repl/components/panel/Reference.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/repl/components/panel/Reference.jsx b/website/src/repl/components/panel/Reference.jsx index 18721eb43..ab925eb91 100644 --- a/website/src/repl/components/panel/Reference.jsx +++ b/website/src/repl/components/panel/Reference.jsx @@ -70,7 +70,7 @@ export function Reference() {
    {entry.params?.map(({ name, type, description }, i) => (
  • - {name} : {type.names?.join(' | ')} {description ? <> - {getInnerText(description)} : ''} + {name} : {type?.names?.join(' | ')} {description ? <> - {getInnerText(description)} : ''}
  • ))}
From 351314ccdf8a52831393724b107c26d1f9a76d13 Mon Sep 17 00:00:00 2001 From: alex Date: Sun, 15 Jun 2025 09:23:29 +0100 Subject: [PATCH 136/174] rename .github to .forgejo --- {.github => .forgejo}/FUNDING.yml | 0 {.github => .forgejo}/workflows/deploy.yml | 0 {.github => .forgejo}/workflows/test.yml | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename {.github => .forgejo}/FUNDING.yml (100%) rename {.github => .forgejo}/workflows/deploy.yml (100%) rename {.github => .forgejo}/workflows/test.yml (100%) diff --git a/.github/FUNDING.yml b/.forgejo/FUNDING.yml similarity index 100% rename from .github/FUNDING.yml rename to .forgejo/FUNDING.yml diff --git a/.github/workflows/deploy.yml b/.forgejo/workflows/deploy.yml similarity index 100% rename from .github/workflows/deploy.yml rename to .forgejo/workflows/deploy.yml diff --git a/.github/workflows/test.yml b/.forgejo/workflows/test.yml similarity index 100% rename from .github/workflows/test.yml rename to .forgejo/workflows/test.yml From 184cb746f331c16800ac97664dfbfd0e997efc03 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Wed, 18 Jun 2025 19:55:30 +0200 Subject: [PATCH 137/174] arch --- packages/codemirror/themes.mjs | 8 ++++ packages/codemirror/themes/archBtw.mjs | 39 ++++++++++++++++++ .../codemirror/themes/bluescreenlight.mjs | 40 +++++++++++++++++++ 3 files changed, 87 insertions(+) create mode 100644 packages/codemirror/themes/archBtw.mjs create mode 100644 packages/codemirror/themes/bluescreenlight.mjs diff --git a/packages/codemirror/themes.mjs b/packages/codemirror/themes.mjs index 6aa70471d..c3763a643 100644 --- a/packages/codemirror/themes.mjs +++ b/packages/codemirror/themes.mjs @@ -8,6 +8,9 @@ import CutiePi, { settings as CutiePiSettings } from './themes/CutiePi.mjs'; import sonicPink, { settings as sonicPinkSettings } from './themes/sonic-pink.mjs'; import redText, { settings as redTextSettings } from './themes/red-text.mjs'; import greenText, { settings as greenTextSettings } from './themes/green-text.mjs'; +import archBtw, { settings as archBtwSettings } from './themes/archBtw.mjs'; +import bluescreenlight, { settings as bluescreenlightsettings } from './themes/bluescreenlight.mjs'; + import androidstudio, { settings as androidstudioSettings } from './themes/androidstudio.mjs'; import atomone, { settings as atomOneSettings } from './themes/atomone.mjs'; import aura, { settings as auraSettings } from './themes/aura.mjs'; @@ -39,12 +42,14 @@ import { setTheme } from '@strudel/draw'; export const themes = { strudelTheme, algoboy, + archBtw, androidstudio, atomone, aura, bbedit, blackscreen, bluescreen, + bluescreenlight, CutiePi, darcula, dracula, @@ -78,10 +83,12 @@ export const themes = { export const settings = { strudelTheme: strudelThemeSettings, bluescreen: bluescreenSettings, + bluescreenlight: bluescreenlightsettings, blackscreen: blackscreenSettings, whitescreen: whitescreenSettings, teletext: teletextSettings, algoboy: algoboySettings, + archBtw: archBtwSettings, androidstudio: androidstudioSettings, atomone: atomOneSettings, aura: auraSettings, @@ -95,6 +102,7 @@ export const settings = { githubLight: githubLightSettings, githubDark: githubDarkSettings, greenText: greenTextSettings, + gruvboxDark: gruvboxDarkSettings, gruvboxLight: gruvboxLightSettings, materialDark: materialDarkSettings, diff --git a/packages/codemirror/themes/archBtw.mjs b/packages/codemirror/themes/archBtw.mjs new file mode 100644 index 000000000..7f4c1b5e3 --- /dev/null +++ b/packages/codemirror/themes/archBtw.mjs @@ -0,0 +1,39 @@ +/* + * Atom One + * Atom One dark syntax theme + * + * https://github.com/atom/one-dark-syntax + */ +import { tags as t } from '@lezer/highlight'; +import { createTheme } from './theme-helper.mjs'; + +const hex = ['rgb(0, 0, 0)', 'rgb(113, 208, 250)', 'rgba(113, 208, 250, .4)', 'rgba(113, 208, 250, .15)']; + +export const settings = { + background: hex[0], + lineBackground: 'transparent', + foreground: hex[1], + selection: hex[2], + selectionMatch: hex[0], + gutterBackground: hex[0], + gutterForeground: hex[2], + gutterBorder: 'transparent', + lineHighlight: hex[0], +}; + +export default createTheme({ + theme: 'dark', + settings, + styles: [ + { + tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction], + color: hex[1], + }, + { tag: [t.atom, t.bool, t.special(t.variableName)], color: hex[1] }, + { tag:[ t.comment, t.brace, t.bracket, ], color: hex[2] }, + { tag: [t.variableName, t.propertyName, t.labelName], color: hex[1] }, + { tag: [t.attributeName, t.number], color: hex[1] }, + { tag: t.keyword, color: hex[1] }, + { tag: [t.string, t.regexp, t.special(t.propertyName)], color: hex[1] }, + ], +}); diff --git a/packages/codemirror/themes/bluescreenlight.mjs b/packages/codemirror/themes/bluescreenlight.mjs new file mode 100644 index 000000000..e78bf707c --- /dev/null +++ b/packages/codemirror/themes/bluescreenlight.mjs @@ -0,0 +1,40 @@ +/* + * Atom One + * Atom One dark syntax theme + * + * https://github.com/atom/one-dark-syntax + */ +import { tags as t } from '@lezer/highlight'; +import { createTheme } from './theme-helper.mjs'; + +const hex = ['rgb(75, 130, 247)', 'rgb(47, 108, 246)', 'rgb(255, 255, 255)', 'rgba(255, 255, 255,.3)']; + +export const settings = { + background: hex[0], + lineBackground: 'transparent', + foreground: hex[2], + selection: hex[3], + selectionMatch: hex[0], + gutterBackground: hex[0], + gutterForeground: hex[2], + gutterBorder: 'transparent', + lineHighlight: hex[1], +}; + +export default createTheme({ + theme: 'dark', + settings, + styles: [ + { + tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction], + color: hex[2], + + }, + { tag: [t.atom, t.bool, t.special(t.variableName)], color: hex[2] }, + { tag: [t.comment, t.bracket, t.brace, t.compareOperator], color: hex[3] }, + { tag: [t.variableName, t.propertyName, t.labelName], color: hex[2] }, + { tag: [t.attributeName, t.number], color: hex[2] }, + { tag: t.keyword, color: hex[2] }, + { tag: [t.string, t.regexp, t.special(t.propertyName)], color: hex[2] }, + ], +}); From 011139eff8049abf81af37d169a19cfdbff8e4ed Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Wed, 18 Jun 2025 20:12:36 +0200 Subject: [PATCH 138/174] arch2 --- packages/codemirror/themes/archBtw.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/codemirror/themes/archBtw.mjs b/packages/codemirror/themes/archBtw.mjs index 7f4c1b5e3..1ed6a0c6a 100644 --- a/packages/codemirror/themes/archBtw.mjs +++ b/packages/codemirror/themes/archBtw.mjs @@ -7,7 +7,7 @@ import { tags as t } from '@lezer/highlight'; import { createTheme } from './theme-helper.mjs'; -const hex = ['rgb(0, 0, 0)', 'rgb(113, 208, 250)', 'rgba(113, 208, 250, .4)', 'rgba(113, 208, 250, .15)']; +const hex = ['rgb(0, 0, 0)', 'rgb(82, 208, 250)', 'rgba(113, 208, 250, .4)', 'rgba(113, 208, 250, .15)']; export const settings = { background: hex[0], From c751bb7e5f06a63f47aa134d671943efef952de1 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 18 Jun 2025 21:36:34 +0200 Subject: [PATCH 139/174] allow _ as silence --- packages/mondo/mondo.mjs | 3 ++- packages/mondough/mondough.mjs | 1 + packages/superdough/superdough.mjs | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 73dbf1c7a..e3f62846b 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -20,7 +20,8 @@ export class MondoParser { open_curly: /^\{/, close_curly: /^\}/, number: /^-?[0-9]*\.?[0-9]+/, // before pipe! - op: /^[*/:!@%?+-]|^\.{2}/, // * / : ! @ % ? .. + // "+" and "-" might be added here, but then "-" won't work as silence anymore.. + op: /^[*/:!@%?]|^\.{2}/, // * / : ! @ % ? .. // dollar: /^\$/, pipe: /^#/, stack: /^[,$]/, diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index 3731e8cee..cd0521bad 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -28,6 +28,7 @@ let nope = (...args) => args[args.length - 1]; let lib = {}; lib['nope'] = nope; lib['-'] = silence; +lib['_'] = silence; lib['~'] = silence; lib.curly = stepcat; lib.square = (...args) => stepcat(...args).setSteps(1); diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 61be0b4e8..90bb5e701 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -575,7 +575,7 @@ export const superdough = async (value, t, hapDuration, cps) => { let audioNodes = []; - if (['-', '~'].includes(s)) { + if (['-', '~', '_'].includes(s)) { return; } if (bank && s) { From 9412bf60bfa63d7c4c1cbb1db4507e84af352123 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 18 Jun 2025 22:06:18 +0200 Subject: [PATCH 140/174] add e as euclid that accepts a list --- packages/core/euclid.mjs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/core/euclid.mjs b/packages/core/euclid.mjs index 2ee9962da..25e12b07c 100644 --- a/packages/core/euclid.mjs +++ b/packages/core/euclid.mjs @@ -139,6 +139,14 @@ export const euclid = register('euclid', function (pulses, steps, pat) { return pat.struct(_euclidRot(pulses, steps, 0)); }); +export const e = register('e', function (euc, pat) { + if (!Array.isArray(euc)) { + euc = [euc]; + } + const [pulses, steps = pulses, rot = 0] = euc; + return pat.struct(_euclidRot(pulses, steps, rot)); +}); + export const { euclidrot, euclidRot } = register(['euclidrot', 'euclidRot'], function (pulses, steps, rotation, pat) { return pat.struct(_euclidRot(pulses, steps, rotation)); }); From 10ac7c353cb16034bce8da900c60702e431e4c9f Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 18 Jun 2025 22:07:16 +0200 Subject: [PATCH 141/174] use + and - as late / early + add some extra error handling --- packages/mondo/mondo.mjs | 16 ++++++++++++++-- packages/mondough/mondough.mjs | 3 ++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index e3f62846b..b879f24bf 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -20,8 +20,8 @@ export class MondoParser { open_curly: /^\{/, close_curly: /^\}/, number: /^-?[0-9]*\.?[0-9]+/, // before pipe! - // "+" and "-" might be added here, but then "-" won't work as silence anymore.. - op: /^[*/:!@%?]|^\.{2}/, // * / : ! @ % ? .. + // TODO: better error handling when "-" is used as rest, e.g "s [- bd]" + op: /^[*/:!@%?+-]|^\.{2}/, // * / : ! @ % ? .. // dollar: /^\$/, pipe: /^#/, stack: /^[,$]/, @@ -173,6 +173,18 @@ export class MondoParser { children[opIndex] = op; continue; } + // some careful error handling + if (left.type === 'op') { + throw new Error(`got 2 ops in a row: "${left.value}${op.value}"`); + } + if (right.type === 'op') { + let err = `got 2 ops in a row: "${op.value}${right.value}"`; + if (op.value === '-') { + // yes i know this file is not supposed to know about rests x.X + err += '. you probably want a rest, which is "_" in mondo!'; + } + throw new Error(err); + } const call = { type: 'list', children: [op, right, left] }; // insert call while keeping other siblings children = [...children.slice(0, opIndex - 1), call, ...children.slice(opIndex + 2)]; diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index cd0521bad..a4e7c93b3 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -27,7 +27,8 @@ let nope = (...args) => args[args.length - 1]; let lib = {}; lib['nope'] = nope; -lib['-'] = silence; +lib['-'] = (a, b) => b.early(a); +lib['+'] = (a, b) => b.late(a); lib['_'] = silence; lib['~'] = silence; lib.curly = stepcat; From 1cc15c7e8dd6e20e9aeff072b827c96f3df80fd4 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 18 Jun 2025 22:25:05 +0200 Subject: [PATCH 142/174] fix: fall back to silence when empty removes annoying error --- packages/transpiler/transpiler.mjs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/transpiler/transpiler.mjs b/packages/transpiler/transpiler.mjs index b0061c600..fea6bad4d 100644 --- a/packages/transpiler/transpiler.mjs +++ b/packages/transpiler/transpiler.mjs @@ -113,8 +113,18 @@ export function transpiler(input, options = {}) { leave(node, parent, prop, index) {}, }); - const { body } = ast; - if (!body?.[body.length - 1]?.expression) { + let { body } = ast; + + if (!body.length) { + console.warn('empty body -> fallback to silence'); + body.push({ + type: 'ExpressionStatement', + expression: { + type: 'Identifier', + name: 'silence', + }, + }); + } else if (!body?.[body.length - 1]?.expression) { throw new Error('unexpected ast format without body expression'); } From 7e6876c2824071b2603b63e80675d9be9a2e4bc9 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Wed, 18 Jun 2025 20:44:09 -0400 Subject: [PATCH 143/174] comment --- packages/codemirror/themes.mjs | 4 ++ packages/codemirror/themes/archBtw.mjs | 9 ++-- .../codemirror/themes/bluescreenlight.mjs | 11 ++-- packages/codemirror/themes/fruitDaw.mjs | 50 +++++++++++++++++++ 4 files changed, 62 insertions(+), 12 deletions(-) create mode 100644 packages/codemirror/themes/fruitDaw.mjs diff --git a/packages/codemirror/themes.mjs b/packages/codemirror/themes.mjs index c3763a643..1a523b391 100644 --- a/packages/codemirror/themes.mjs +++ b/packages/codemirror/themes.mjs @@ -9,6 +9,8 @@ import sonicPink, { settings as sonicPinkSettings } from './themes/sonic-pink.mj import redText, { settings as redTextSettings } from './themes/red-text.mjs'; import greenText, { settings as greenTextSettings } from './themes/green-text.mjs'; import archBtw, { settings as archBtwSettings } from './themes/archBtw.mjs'; +import fruitDaw, { settings as fruitDawSettings } from './themes/fruitDaw.mjs'; + import bluescreenlight, { settings as bluescreenlightsettings } from './themes/bluescreenlight.mjs'; import androidstudio, { settings as androidstudioSettings } from './themes/androidstudio.mjs'; @@ -55,6 +57,7 @@ export const themes = { dracula, duotoneDark, eclipse, + fruitDaw, githubDark, githubLight, greenText, @@ -99,6 +102,7 @@ export const settings = { eclipse: eclipseSettings, CutiePi: CutiePiSettings, sonicPink: sonicPinkSettings, + fruitDaw: fruitDawSettings, githubLight: githubLightSettings, githubDark: githubDarkSettings, greenText: greenTextSettings, diff --git a/packages/codemirror/themes/archBtw.mjs b/packages/codemirror/themes/archBtw.mjs index 1ed6a0c6a..87546569c 100644 --- a/packages/codemirror/themes/archBtw.mjs +++ b/packages/codemirror/themes/archBtw.mjs @@ -1,8 +1,7 @@ /* - * Atom One - * Atom One dark syntax theme - * - * https://github.com/atom/one-dark-syntax + * Arch Btw + * Modern terminal inspired theme + * made by Jade */ import { tags as t } from '@lezer/highlight'; import { createTheme } from './theme-helper.mjs'; @@ -30,7 +29,7 @@ export default createTheme({ color: hex[1], }, { tag: [t.atom, t.bool, t.special(t.variableName)], color: hex[1] }, - { tag:[ t.comment, t.brace, t.bracket, ], color: hex[2] }, + { tag: [t.comment, t.brace, t.bracket], color: hex[2] }, { tag: [t.variableName, t.propertyName, t.labelName], color: hex[1] }, { tag: [t.attributeName, t.number], color: hex[1] }, { tag: t.keyword, color: hex[1] }, diff --git a/packages/codemirror/themes/bluescreenlight.mjs b/packages/codemirror/themes/bluescreenlight.mjs index e78bf707c..031a3da33 100644 --- a/packages/codemirror/themes/bluescreenlight.mjs +++ b/packages/codemirror/themes/bluescreenlight.mjs @@ -1,13 +1,11 @@ /* - * Atom One - * Atom One dark syntax theme - * - * https://github.com/atom/one-dark-syntax + * A lighter blue screen theme + * made by Jade */ import { tags as t } from '@lezer/highlight'; import { createTheme } from './theme-helper.mjs'; -const hex = ['rgb(75, 130, 247)', 'rgb(47, 108, 246)', 'rgb(255, 255, 255)', 'rgba(255, 255, 255,.3)']; +const hex = ['rgb(75, 130, 247)', 'rgb(47, 108, 246)', 'rgb(255, 255, 255)', 'rgba(255, 255, 255,.3)']; export const settings = { background: hex[0], @@ -28,9 +26,8 @@ export default createTheme({ { tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction], color: hex[2], - }, - { tag: [t.atom, t.bool, t.special(t.variableName)], color: hex[2] }, + { tag: [t.atom, t.bool, t.special(t.variableName)], color: hex[2] }, { tag: [t.comment, t.bracket, t.brace, t.compareOperator], color: hex[3] }, { tag: [t.variableName, t.propertyName, t.labelName], color: hex[2] }, { tag: [t.attributeName, t.number], color: hex[2] }, diff --git a/packages/codemirror/themes/fruitDaw.mjs b/packages/codemirror/themes/fruitDaw.mjs new file mode 100644 index 000000000..c9e557796 --- /dev/null +++ b/packages/codemirror/themes/fruitDaw.mjs @@ -0,0 +1,50 @@ +/* + * Fruit Daw + * made by Jade + */ +import { tags as t } from '@lezer/highlight'; +import { createTheme } from './theme-helper.mjs'; + +const hex = [ + 'rgb(84, 93, 98)', + 'rgb(255, 255, 255)', + 'rgba(255, 255, 255, .25)', + 'rgb(67, 76, 81)', + 'rgb(186, 230, 115)', + 'rgb(252, 184, 67)', + 'rgb(124, 206, 254)', + 'rgb(83, 101, 102)', + 'rgba(46, 62, 72,.5)', + 'rgb(94, 100, 108)', + 'rgb(167, 216, 177)', +]; + +export const settings = { + background: hex[0], + lineBackground: 'transparent', + foreground: hex[10], + selection: hex[8], + selectionMatch: hex[0], + gutterBackground: hex[3], + gutterForeground: hex[2], + gutterBorder: 'transparent', + lineHighlight: hex[3], +}; + +export default createTheme({ + theme: 'dark', + settings, + styles: [ + { + tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction], + color: hex[1], + }, + { tag: [t.bool, t.special(t.variableName)], color: hex[1] }, + { tag: [t.comment, t.brace, t.bracket], color: hex[2] }, + { tag: [t.variableName], color: hex[1] }, + { tag: [t.labelName, t.propertyName, t.self, t.atom], color: hex[5] }, + { tag: [t.attributeName, t.number], color: hex[6] }, + { tag: t.keyword, color: hex[5] }, + { tag: [t.string, t.regexp, t.special(t.propertyName)], color: hex[4] }, + ], +}); From 6c4aa12c6045fc7a6e0fa086e124124ce194bf0c Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 19 Jun 2025 09:09:45 +0200 Subject: [PATCH 144/174] refactor: replace .cpm with setcpm calls + doc setcpm --- packages/core/pattern.mjs | 1 + packages/core/repl.mjs | 11 ++++ packages/gamepad/docs/gamepad.mdx | 15 ++--- .../src/pages/de/workshop/first-sounds.mdx | 56 ++++++++++++------- website/src/pages/de/workshop/recap.mdx | 20 +++---- website/src/pages/learn/samples.mdx | 10 ++-- website/src/pages/understand/cycles.mdx | 49 +++++++++++----- website/src/pages/workshop/first-notes.mdx | 22 +++++--- website/src/pages/workshop/first-sounds.mdx | 44 ++++++++++----- .../src/pages/workshop/pattern-effects.mdx | 10 ++-- website/src/pages/workshop/recap.mdx | 20 +++---- website/src/repl/tunes.mjs | 20 ------- 12 files changed, 169 insertions(+), 109 deletions(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 76b02c21e..41b09ea20 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -1981,6 +1981,7 @@ export const apply = register('apply', function (func, pat) { /** * Plays the pattern at the given cycles per minute. + * @deprecated * @example * s(",hh*2").cpm(90) // = 90 bpm */ diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 7a8cbbab2..8acd2bbea 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -85,6 +85,17 @@ export function repl({ const pause = () => scheduler.pause(); const toggle = () => scheduler.toggle(); const setCps = (cps) => scheduler.setCps(cps); + + /** + * Changes the global tempo to the given cycles per minute + * + * @name setcpm + * @alias setCpm + * @param {number} cpm cycles per minute + * @example + * setcpm(140/4) // =140 bpm in 4/4 + * $: s("bd*4,[- sd]*2").bank('tr707') + */ const setCpm = (cpm) => scheduler.setCps(cpm / 60); // TODO - not documented as jsdoc examples as the test framework doesn't simulate enough context for `each` and `all`.. diff --git a/packages/gamepad/docs/gamepad.mdx b/packages/gamepad/docs/gamepad.mdx index 219d8ae0b..e2da594ea 100644 --- a/packages/gamepad/docs/gamepad.mdx +++ b/packages/gamepad/docs/gamepad.mdx @@ -56,14 +56,15 @@ You can use button inputs to control different aspects of your music, such as ga @@ -74,14 +75,13 @@ Analog sticks can be used for continuous control, such as pitch shifting or pann ### Button Sequences @@ -89,6 +89,7 @@ $: note("c4 d3 a3 e3").sound("sawtooth") You can define button sequences to trigger specific actions, like playing a sound when a sequence is detected. -**Tempo ändern mit `cpm`** +**Tempo ändern mit `setcpm`** -*8").cpm(90/4)`} punchcard /> +*8")`} + punchcard +/> @@ -233,8 +238,9 @@ Bolero: @@ -313,18 +319,23 @@ Das haben wir bisher gelernt: Die mit Apostrophen umgebene Mini-Notation benutzt man normalerweise in einer sogenannten Funktion. Die folgenden Funktionen haben wir bereits gesehen: -| Name | Description | Example | -| ----- | -------------------------------------- | ----------------------------------------------------------------------- | -| sound | Spielt den Sound mit dem Namen | | -| bank | Wählt die Soundbank / Drum Machine | | -| cpm | Tempo in **C**ycles **p**ro **M**inute | | -| n | Sample Nummer | | +| Name | Description | Example | +| ------ | -------------------------------------- | ----------------------------------------------------------------------- | +| sound | Spielt den Sound mit dem Namen | | +| bank | Wählt die Soundbank / Drum Machine | | +| setcpm | Tempo in **C**ycles **p**ro **M**inute | | +| n | Sample Nummer | | ## Beispiele **Einfacher Rock Beat** - + **Klassischer House** @@ -339,7 +350,12 @@ Bestimmte Drum Patterns werden oft genreübergreifend wiederverwendet. We Will Rock you - + **Yellow Magic Orchestra - Firecracker** @@ -354,12 +370,13 @@ We Will Rock you @@ -367,12 +384,13 @@ We Will Rock you @@ -380,11 +398,11 @@ We Will Rock you diff --git a/website/src/pages/de/workshop/recap.mdx b/website/src/pages/de/workshop/recap.mdx index 14ef6b252..ec279c084 100644 --- a/website/src/pages/de/workshop/recap.mdx +++ b/website/src/pages/de/workshop/recap.mdx @@ -56,13 +56,13 @@ Diese Seite ist eine Auflistung aller im Workshop vorgestellten Funktionen. ## Pattern-Effekte -| Name | Beschreibung | Beispiel | -| ---- | --------------------------------- | ----------------------------------------------------------------------------------- | -| cpm | Tempo in Cycles pro Minute | | -| fast | schneller | | -| slow | langsamer | | -| rev | rückwärts | | -| jux | einen Stereo-Kanal modifizieren | | -| add | addiert Zahlen oder Noten | ")).scale("C:minor")`} /> | -| ply | jedes Element schneller machen | ")`} /> | -| off | verzögert eine modifizierte Kopie | x.speed(2))`} /> | +| Name | Beschreibung | Beispiel | +| ------ | --------------------------------- | ----------------------------------------------------------------------------------- | +| setcpm | Tempo in Cycles pro Minute | | +| fast | schneller | | +| slow | langsamer | | +| rev | rückwärts | | +| jux | einen Stereo-Kanal modifizieren | | +| add | addiert Zahlen oder Noten | ")).scale("C:minor")`} /> | +| ply | jedes Element schneller machen | ")`} /> | +| off | verzögert eine modifizierte Kopie | x.speed(2))`} /> | diff --git a/website/src/pages/learn/samples.mdx b/website/src/pages/learn/samples.mdx index cd7944caa..ef97ec0ce 100644 --- a/website/src/pages/learn/samples.mdx +++ b/website/src/pages/learn/samples.mdx @@ -247,15 +247,17 @@ We can also declare different samples for different regions of the keyboard: !2, g4 f4]>") - .s('moog').clip(1) - .gain(.5).cpm(60)`} +.s('moog').clip(1) +.gain(.5)`} /> The sampler will always pick the closest matching sample for the current note! diff --git a/website/src/pages/understand/cycles.mdx b/website/src/pages/understand/cycles.mdx index 4694850c3..1c0a778a6 100644 --- a/website/src/pages/understand/cycles.mdx +++ b/website/src/pages/understand/cycles.mdx @@ -43,21 +43,33 @@ This is why the same CPS can produce different perceived tempos. ## Setting CPM -If you're familiar with BPM, you can use the `cpm` method to set the tempo in cycles per minute: +If you're familiar with BPM, you can use the `setcpm` method to set the global tempo in cycles per minute: - + If you want to add more beats per cycle, you might want to divide the cpm: - + Or using 2 beats per cycle: - + -To set a specific bpm, use `.cpm(bpm/bpc)` +To set a specific bpm, use `setcpm(bpm/bpc)` - bpm: the target beats per minute - bpc: the number of perceived beats per cycle @@ -74,22 +86,31 @@ Many music programs use it as a default. Strudel does not a have concept of bars or measures, there are only cycles. How you use them is up to you. Above, we've had this example: - + This could be interpreted as 4/4 time with a tempo of 110bpm. We could write out multiple bars like this: \`).cpm(110/4)`} +>\`)`} /> Instead of writing out each bar separately, we could express this much shorter: ->,hh*4").cpm(110/2)`} /> +>,hh*4")`} +/> Here we can see that thinking in cycles rather than bars simplifies things a lot! These types of simplifications work because of the repetitive nature of rhythm. @@ -109,10 +130,11 @@ We could also write multiple bars with different time signatures: \`).cpm(110*2)`} +>\`)`} /> Here we switch between 3/4 and 4/4, keeping the same tempo. @@ -121,10 +143,11 @@ If we don't specify the length, we get what's called a metric modulation: \`).cpm(110/2)`} +>\`)`} /> Now the 3 elements get the same time as the 4 elements, which is why the tempo changes. diff --git a/website/src/pages/workshop/first-notes.mdx b/website/src/pages/workshop/first-notes.mdx index f3d74ddce..56312a355 100644 --- a/website/src/pages/workshop/first-notes.mdx +++ b/website/src/pages/workshop/first-notes.mdx @@ -216,8 +216,9 @@ Finding the right notes can be difficult.. Scales are here to help: ") -.scale("C:minor").sound("piano").cpm(60)`} + tune={`setcpm(60) +n("0 2 4 <[6,8] [7,9]>") +.scale("C:minor").sound("piano")`} punchcard /> @@ -242,9 +243,10 @@ Just like anything, we can automate the scale with a pattern: , 2 4 <[6,8] [7,9]>") + tune={`setcpm(60) +n("<0 -3>, 2 4 <[6,8] [7,9]>") .scale("/4") -.sound("piano").cpm(60)`} +.sound("piano")`} punchcard /> @@ -275,9 +277,10 @@ Try changing that number! *2") + tune={`setcpm(60) +n("<[4@2 4] [5@2 5] [6@2 6] [5@2 5]>*2") .scale("/4") -.sound("gm_acoustic_bass").cpm(60)`} +.sound("gm_acoustic_bass")`} punchcard /> @@ -291,7 +294,12 @@ This is also sometimes called triplet swing. You'll often find it in blues and j **Replicate** -]").sound("piano").cpm(60)`} punchcard /> +]").sound("piano")`} + punchcard +/> diff --git a/website/src/pages/workshop/first-sounds.mdx b/website/src/pages/workshop/first-sounds.mdx index 9c33cdc3c..083a55390 100644 --- a/website/src/pages/workshop/first-sounds.mdx +++ b/website/src/pages/workshop/first-sounds.mdx @@ -166,9 +166,14 @@ Try also changing the number at the end to change the tempo! -**changing the tempo with cpm** +**changing the tempo with setcpm** -*8").cpm(90/4)`} punchcard /> +*8")`} + punchcard +/> @@ -287,7 +292,7 @@ The Mini-Notation is usually used inside some function. These are the functions | ----- | ----------------------------------- | --------------------------------------------------------------------------------- | | sound | plays the sound of the given name | | | bank | selects the sound bank | | -| cpm | sets the tempo in cycles per minute | | +| cpm | sets the tempo in cycles per minute | | | n | select sample number | | ## Examples @@ -296,8 +301,8 @@ The Mini-Notation is usually used inside some function. These are the functions @@ -314,14 +319,20 @@ Certain drum patterns are reused across genres. We Will Rock you - + **Yellow Magic Orchestra - Firecracker** @@ -329,12 +340,14 @@ We Will Rock you @@ -342,12 +355,13 @@ We Will Rock you @@ -355,11 +369,11 @@ We Will Rock you diff --git a/website/src/pages/workshop/pattern-effects.mdx b/website/src/pages/workshop/pattern-effects.mdx index c9cb7b462..ad84a9ab3 100644 --- a/website/src/pages/workshop/pattern-effects.mdx +++ b/website/src/pages/workshop/pattern-effects.mdx @@ -68,9 +68,10 @@ Try commenting out one or more by adding `//` before a line >")) + tune={`setcpm(60) +note("c2 [eb3,g3] ".add("<0 <1 -1>>")) .color(">").adsr("[.1 0]:.2:[1 0]") -.sound("gm_acoustic_bass").room(.5).cpm(60)`} +.sound("gm_acoustic_bass").room(.5)`} punchcard /> @@ -84,9 +85,10 @@ We can add as often as we like: >").add("0,7")) + tune={`setcpm(60) +note("c2 [eb3,g3]".add("<0 <1 -1>>").add("0,7")) .color(">").adsr("[.1 0]:.2:[1 0]") -.sound("gm_acoustic_bass").room(.5).cpm(60)`} +.sound("gm_acoustic_bass").room(.5)`} punchcard /> diff --git a/website/src/pages/workshop/recap.mdx b/website/src/pages/workshop/recap.mdx index e2b661f56..08e243cef 100644 --- a/website/src/pages/workshop/recap.mdx +++ b/website/src/pages/workshop/recap.mdx @@ -56,13 +56,13 @@ This page is just a listing of all functions covered in the workshop! ## Pattern Effects -| name | description | example | -| ---- | ----------------------------------- | ----------------------------------------------------------------------------------- | -| cpm | sets the tempo in cycles per minute | | -| fast | speed up | | -| slow | slow down | | -| rev | reverse | | -| jux | split left/right, modify right | | -| add | add numbers / notes | ")).scale("C:minor")`} /> | -| ply | speed up each event n times | ")`} /> | -| off | copy, shift time & modify | x.speed(2))`} /> | +| name | description | example | +| ------ | ----------------------------------- | ----------------------------------------------------------------------------------- | +| setcpm | sets the tempo in cycles per minute | | +| fast | speed up | | +| slow | slow down | | +| rev | reverse | | +| jux | split left/right, modify right | | +| add | add numbers / notes | ")).scale("C:minor")`} /> | +| ply | speed up each event n times | ")`} /> | +| off | copy, shift time & modify | x.speed(2))`} /> | diff --git a/website/src/repl/tunes.mjs b/website/src/repl/tunes.mjs index 774e04e49..afaf937df 100644 --- a/website/src/repl/tunes.mjs +++ b/website/src/repl/tunes.mjs @@ -313,26 +313,6 @@ stack( ) .fast(2/3) .pianoroll()`; -/* -export const bridgeIsOver = `// "Bridge is over" -// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ -// @by Felix Roos, bassline by BDP - The Bridge Is Over - -samples({mad:'https://freesound.org/data/previews/22/22274_109943-lq.mp3'}) -stack( - stack( - note("c3*2 [[c3@1.4 bb2] ab2] gb2*2 <[[gb2@1.4 ab2] bb2] gb2>") - .gain(.8).clip("[.5 1]*2"), - n("<0 1 2 3 4 3 2 1>") - .clip(.5) - .echoWith(8, 1/32, (x,i)=>x.add(n(i)).velocity(Math.pow(.7,i))) - .scale('c4 whole tone') - .echo(3, 1/8, .5) - ).piano(), - s("mad").slow(2) -).cpm(78).slow(4) - .pianoroll() -`; */ export const goodTimes = `// "Good times" // @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ From 64ab3c9ba73fddfe3d554b376a72a8ac6dfb23f5 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 19 Jun 2025 09:12:05 +0200 Subject: [PATCH 145/174] fix: tests --- test/__snapshots__/examples.test.mjs.snap | 29 +++++++++++++++++++++++ test/runtime.mjs | 2 ++ 2 files changed, 31 insertions(+) diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 85948f0c0..f3607cb48 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -8450,6 +8450,35 @@ exports[`runs examples > example "seqPLoop" example index 0 1`] = ` ] `; +exports[`runs examples > example "setcpm" example index 0 1`] = ` +[ + "[ 0/1 → 1/4 | s:bd bank:tr707 ]", + "[ 1/4 → 1/2 | s:bd bank:tr707 ]", + "[ 1/4 → 1/2 | s:sd bank:tr707 ]", + "[ 1/2 → 3/4 | s:bd bank:tr707 ]", + "[ 3/4 → 1/1 | s:bd bank:tr707 ]", + "[ 3/4 → 1/1 | s:sd bank:tr707 ]", + "[ 1/1 → 5/4 | s:bd bank:tr707 ]", + "[ 5/4 → 3/2 | s:bd bank:tr707 ]", + "[ 5/4 → 3/2 | s:sd bank:tr707 ]", + "[ 3/2 → 7/4 | s:bd bank:tr707 ]", + "[ 7/4 → 2/1 | s:bd bank:tr707 ]", + "[ 7/4 → 2/1 | s:sd bank:tr707 ]", + "[ 2/1 → 9/4 | s:bd bank:tr707 ]", + "[ 9/4 → 5/2 | s:bd bank:tr707 ]", + "[ 9/4 → 5/2 | s:sd bank:tr707 ]", + "[ 5/2 → 11/4 | s:bd bank:tr707 ]", + "[ 11/4 → 3/1 | s:bd bank:tr707 ]", + "[ 11/4 → 3/1 | s:sd bank:tr707 ]", + "[ 3/1 → 13/4 | s:bd bank:tr707 ]", + "[ 13/4 → 7/2 | s:bd bank:tr707 ]", + "[ 13/4 → 7/2 | s:sd bank:tr707 ]", + "[ 7/2 → 15/4 | s:bd bank:tr707 ]", + "[ 15/4 → 4/1 | s:bd bank:tr707 ]", + "[ 15/4 → 4/1 | s:sd bank:tr707 ]", +] +`; + exports[`runs examples > example "shape" example index 0 1`] = ` [ "[ 0/1 → 1/8 | s:hh shape:0 ]", diff --git a/test/runtime.mjs b/test/runtime.mjs index 6b75fb3be..2a29de3f5 100644 --- a/test/runtime.mjs +++ b/test/runtime.mjs @@ -97,6 +97,7 @@ const toneHelpersMocked = { '_pianoroll', '_spectrum', 'markcss', + 'p', ].forEach((mock) => { strudel.Pattern.prototype[mock] = function () { return this; @@ -163,6 +164,7 @@ evalScope( loadCsound, loadcsound, setcps: id, + setcpm: id, Clock: {}, // whatever }, ); From 66673d211ea60716d83d483b80fdb353c0ba9538 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 19 Jun 2025 09:13:48 +0200 Subject: [PATCH 146/174] fix: cpm -> setcpm --- website/src/pages/workshop/first-sounds.mdx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/website/src/pages/workshop/first-sounds.mdx b/website/src/pages/workshop/first-sounds.mdx index 083a55390..74daf4bab 100644 --- a/website/src/pages/workshop/first-sounds.mdx +++ b/website/src/pages/workshop/first-sounds.mdx @@ -288,12 +288,12 @@ This is what we've learned so far: The Mini-Notation is usually used inside some function. These are the functions we've seen so far: -| Name | Description | Example | -| ----- | ----------------------------------- | --------------------------------------------------------------------------------- | -| sound | plays the sound of the given name | | -| bank | selects the sound bank | | -| cpm | sets the tempo in cycles per minute | | -| n | select sample number | | +| Name | Description | Example | +| ------ | ----------------------------------- | --------------------------------------------------------------------------------- | +| sound | plays the sound of the given name | | +| bank | selects the sound bank | | +| setcpm | sets the tempo in cycles per minute | | +| n | select sample number | | ## Examples From 95193b0bfd4b4e2c2e0c6ef216b4331e2f037cf4 Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 19 Jun 2025 08:18:20 +0100 Subject: [PATCH 147/174] a test for #1396 --- packages/core/test/pattern.test.mjs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs index 45a1e2a98..b93d80f25 100644 --- a/packages/core/test/pattern.test.mjs +++ b/packages/core/test/pattern.test.mjs @@ -52,6 +52,7 @@ import { stackCentre, stepcat, sometimes, + expand } from '../index.mjs'; import { steady } from '../signal.mjs'; @@ -1179,6 +1180,9 @@ describe('Pattern', () => { it('calculates undefined steps as the average', () => { expect(sameFirst(stepcat(pure(1), pure(2), pure(3).setSteps(undefined)), fastcat(1, 2, 3))); }); + it('works with auto-reified values', () => { + expect(sameFirst(stepcat(expand(3, 'bd'), 'rim'), stepcat(expand(3, 'bd'), pure('rim')))) + }); }); describe('shrink', () => { it('can shrink', () => { From 440f1cb081c0947ba3ba8b00b33f4dcc74bafe98 Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 19 Jun 2025 08:27:23 +0100 Subject: [PATCH 148/174] reify arguments of stepcat --- packages/core/pattern.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 76b02c21e..59833aea7 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -2720,6 +2720,7 @@ export function stepcat(...timepats) { return nothing; } const findsteps = (x) => (Array.isArray(x) ? x : [x._steps, x]); + timepats = timepats.map(reify); timepats = timepats.map(findsteps); if (timepats.find((x) => x[0] === undefined)) { const times = timepats.map((a) => a[0]).filter((x) => x !== undefined); From a1d181d6097057542ca7dd943a2a8105330f52d5 Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 19 Jun 2025 08:27:41 +0100 Subject: [PATCH 149/174] format --- packages/core/test/pattern.test.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs index b93d80f25..07bdcdda7 100644 --- a/packages/core/test/pattern.test.mjs +++ b/packages/core/test/pattern.test.mjs @@ -52,7 +52,7 @@ import { stackCentre, stepcat, sometimes, - expand + expand, } from '../index.mjs'; import { steady } from '../signal.mjs'; @@ -1181,7 +1181,7 @@ describe('Pattern', () => { expect(sameFirst(stepcat(pure(1), pure(2), pure(3).setSteps(undefined)), fastcat(1, 2, 3))); }); it('works with auto-reified values', () => { - expect(sameFirst(stepcat(expand(3, 'bd'), 'rim'), stepcat(expand(3, 'bd'), pure('rim')))) + expect(sameFirst(stepcat(expand(3, 'bd'), 'rim'), stepcat(expand(3, 'bd'), pure('rim')))); }); }); describe('shrink', () => { From a8757fecc8554b10274ca27e378fe9bdb2e216fc Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 19 Jun 2025 08:39:05 +0100 Subject: [PATCH 150/174] avoid double action for PRs, try turning on pnpm cache --- .forgejo/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.forgejo/workflows/test.yml b/.forgejo/workflows/test.yml index 765f5958d..1266d2ddf 100644 --- a/.forgejo/workflows/test.yml +++ b/.forgejo/workflows/test.yml @@ -1,6 +1,6 @@ name: Strudel tests -on: [push, pull_request] +on: [push] jobs: build: @@ -17,7 +17,7 @@ jobs: - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - # cache: 'pnpm' + cache: 'pnpm' - run: pnpm install - run: pnpm run format-check - run: pnpm run lint From f3c4afaa54d0c7fa2428ec883052e6643ec725ea Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 19 Jun 2025 08:42:33 +0100 Subject: [PATCH 151/174] apt install zstd for caching --- .forgejo/workflows/test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.forgejo/workflows/test.yml b/.forgejo/workflows/test.yml index 1266d2ddf..90cb7e258 100644 --- a/.forgejo/workflows/test.yml +++ b/.forgejo/workflows/test.yml @@ -11,6 +11,8 @@ jobs: steps: - uses: actions/checkout@v4 + - name: apt install ztd + run: apt update && apt install -y zstd - uses: pnpm/action-setup@v4 with: version: 9.12.2 From c01b4c651c54603fd295fc4750e77d9858123b1e Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 19 Jun 2025 09:06:53 +0100 Subject: [PATCH 152/174] don't trash lists passed to timecat --- packages/core/pattern.mjs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 59833aea7..6441b7568 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -2719,8 +2719,7 @@ export function stepcat(...timepats) { if (timepats.length === 0) { return nothing; } - const findsteps = (x) => (Array.isArray(x) ? x : [x._steps, x]); - timepats = timepats.map(reify); + const findsteps = (x) => (Array.isArray(x) ? x : [x._steps ?? 1, x]); timepats = timepats.map(findsteps); if (timepats.find((x) => x[0] === undefined)) { const times = timepats.map((a) => a[0]).filter((x) => x !== undefined); From 15f3321b04a7ace75294517d6d05605052921afa Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 19 Jun 2025 11:33:22 +0200 Subject: [PATCH 153/174] fix: extend in some situations, like [[bd]@3 bd] --- packages/mondough/mondough.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index a4e7c93b3..6e8278939 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -84,7 +84,7 @@ function evaluator(node, scope) { let pat; if (type === 'plain' && typeof variable !== 'undefined') { // some function names are not patternable, so we skip reification here - if (['!', 'extend', '@', 'expand'].includes(value)) { + if (['!', 'extend', '@', 'expand', 'square', 'angle'].includes(value)) { return variable; } pat = reify(variable); From 4d3bbd5057b81a95f19b75762b2dcf01971d5ba1 Mon Sep 17 00:00:00 2001 From: yaxu Date: Thu, 19 Jun 2025 17:44:45 +0200 Subject: [PATCH 154/174] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3644d53d3..d54302ac6 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ 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. +You will need to abide by the terms of the [GNU Affero Public Licence v3](LICENSE). 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. From e68df9e789bd3d2e1543234777b87f7c2edbfa71 Mon Sep 17 00:00:00 2001 From: Sergei Trofimovich Date: Fri, 20 Jun 2025 18:33:58 +0100 Subject: [PATCH 155/174] website intro: fix whitespace and code hosting name --- website/src/repl/components/panel/WelcomeTab.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/src/repl/components/panel/WelcomeTab.jsx b/website/src/repl/components/panel/WelcomeTab.jsx index cd5fe030a..7f0ee70ab 100644 --- a/website/src/repl/components/panel/WelcomeTab.jsx +++ b/website/src/repl/components/panel/WelcomeTab.jsx @@ -20,7 +20,7 @@ export function WelcomeTab({ context }) {

{/* To learn more about what this all means, check out the{' '} */} - To get started, check out the + To get started, check out the{' '} interactive tutorial @@ -43,7 +43,7 @@ export function WelcomeTab({ context }) { . You can find the source code at{' '} - github + codeberg . You can also find licensing info{' '} for the default sound banks there. Please consider to{' '} From a750764f8847973b9107e72c174c082646d54219 Mon Sep 17 00:00:00 2001 From: yaxu Date: Mon, 23 Jun 2025 11:13:00 +0200 Subject: [PATCH 156/174] Update CONTRIBUTING.md --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index aa84cbce7..f455741f5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,7 +17,7 @@ git remote set-url origin git@codeberg.org:uzu/strudel.git To get in touch with the contributors, either -- [join the Tidal Discord Channel](https://discord.gg/remJ6gQA) and go to the #strudel channel +- [join the Tidal Discord Channel](https://discord.com/invite/HGEdXmRkzT) and go to the #strudel channel - Find related discussions on the [tidal club forum](https://club.tidalcycles.org/) ## Ask a Question From e2a29914c73e7d8c576116e8b5daebb61dcc1773 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Tue, 24 Jun 2025 05:43:06 +0200 Subject: [PATCH 157/174] don't interpret note as n --- packages/tonal/test/tonal.test.mjs | 12 ------------ packages/tonal/tonal.mjs | 9 ++------- 2 files changed, 2 insertions(+), 19 deletions(-) diff --git a/packages/tonal/test/tonal.test.mjs b/packages/tonal/test/tonal.test.mjs index a6cdae391..ec944b339 100644 --- a/packages/tonal/test/tonal.test.mjs +++ b/packages/tonal/test/tonal.test.mjs @@ -30,18 +30,6 @@ describe('tonal', () => { .firstCycleValues.map((h) => h.note), ).toEqual(['C3', 'D3', 'E3']); }); - it('scale with n and note values', () => { - expect( - n(0, 1, 2) - .note(3, 4, 0) - .scale('C major') - .firstCycleValues.map((h) => [h.n, h.note]), - ).toEqual([ - [0, 'F3'], - [1, 'G3'], - [2, 'C3'], - ]); - }); it('scale with colon', () => { expect( n(0, 1, 2) diff --git a/packages/tonal/tonal.mjs b/packages/tonal/tonal.mjs index 2a7f64a8f..27f993bae 100644 --- a/packages/tonal/tonal.mjs +++ b/packages/tonal/tonal.mjs @@ -199,14 +199,9 @@ export const scale = register( pat .fmap((value) => { const isObject = typeof value === 'object'; - let step = value; + let step = isObject ? value.n : value; if (isObject) { - if (typeof value.note !== 'undefined') { - step = value.note; - } else { - step = value.n; - delete value.n; // remove n so it won't cause trouble - } + delete value.n; } if (isNote(step)) { // legacy.. From 71e60ed8defe184c76d398622240c0fddcac5221 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Tue, 24 Jun 2025 05:45:36 +0200 Subject: [PATCH 158/174] add back comment --- packages/tonal/tonal.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tonal/tonal.mjs b/packages/tonal/tonal.mjs index 27f993bae..a425782d2 100644 --- a/packages/tonal/tonal.mjs +++ b/packages/tonal/tonal.mjs @@ -201,7 +201,7 @@ export const scale = register( const isObject = typeof value === 'object'; let step = isObject ? value.n : value; if (isObject) { - delete value.n; + delete value.n; // remove n so it won't cause trouble } if (isNote(step)) { // legacy.. From 0003ce174e612b9ffc689d93c3e918f501981726 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Tue, 24 Jun 2025 06:06:37 +0200 Subject: [PATCH 159/174] allow calling `all` multiple times --- packages/core/repl.mjs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 8acd2bbea..e3d8f6640 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -113,8 +113,9 @@ export function repl({ * all(x => x.pianoroll()) * ``` */ + let allTransforms = []; const all = function (transform) { - allTransform = transform; + allTransforms.push(transform); return silence; }; /** Applies a function to each of the running patterns separately. This is intended for future use with upcoming 'stepwise' features. See `all` for a version that applies the function to all the patterns stacked together into a single pattern. @@ -202,8 +203,10 @@ export function repl({ } else if (eachTransform) { pattern = eachTransform(pattern); } - if (allTransform) { - pattern = allTransform(pattern); + if (allTransforms.length) { + for (let i in allTransforms) { + pattern = allTransforms[i](pattern); + } } if (!isPattern(pattern)) { const message = `got "${typeof evaluated}" instead of pattern`; From d58b3e490b20f08cd5d41fd48c493f13de425939 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Tue, 24 Jun 2025 06:12:21 +0200 Subject: [PATCH 160/174] fix: reset all transforms before eval --- packages/core/repl.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index e3d8f6640..7329979c1 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -191,6 +191,7 @@ export function repl({ await injectPatternMethods(); setTime(() => scheduler.now()); // TODO: refactor? await beforeEval?.({ code }); + allTransforms = []; // reset all transforms shouldHush && hush(); let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions); if (Object.keys(pPatterns).length) { From fe9a91d1e4d9933d21f9aa028851a62f348ebe1e Mon Sep 17 00:00:00 2001 From: alex Date: Tue, 24 Jun 2025 23:22:42 +0100 Subject: [PATCH 161/174] add unjoin, into and chunkinto --- packages/core/pattern.mjs | 41 ++++++++++++++ test/__snapshots__/examples.test.mjs.snap | 69 +++++++++++++++++++++++ 2 files changed, 110 insertions(+) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index beefc5971..0ad1f77c9 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -869,6 +869,47 @@ export class Pattern { console.log(drawLine(this)); return this; } + + ////////////////////////////////////////////////////////////////////// + // methods relating to breaking patterns into subcycles + + // Breaks a pattern into a pattern of patterns, according to the structure of the given binary pattern. + unjoin(pieces, func = id) { + return pieces.withHap((hap) => + hap.withValue((v) => (v ? func(this.ribbon(hap.whole.begin, hap.whole.duration)) : this)), + ); + } + + /** + * Breaks a pattern into pieces according to the structure of a given pattern. + * True values in the given pattern cause the corresponding subcycle of the + * source pattern to be looped, and for an (optional) given function to be + * applied. False values result in the corresponding part of the source pattern + * to be played unchanged. + * @name into + * @memberof Pattern + * @example + * sound("bd sd ht lt").into("1 0", hurry(2)) + */ + into(pieces, func) { + return this.unjoin(pieces, func).innerJoin(); + } + + /** + * Like `chunk`, but the function is applied to a looped subcycle of the source pattern. + * @name chunkInto + * @synonym chunkinto + * @memberof Pattern + * @example + * sound("bd sd ht lt bd - cp lt").chunkInto(4, hurry(2)) + * .bank("tr909") + */ + chunkInto(n, func) { + return this.into(fastcat(true, ...Array(n - 1).fill(false)).iterback(4), func); + } + chunkinto(n, func) { + return this.chunkInto(n, func); + } } ////////////////////////////////////////////////////////////////////// diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index f3607cb48..d0a622940 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -1889,6 +1889,46 @@ exports[`runs examples > example "chunkBack" example index 0 1`] = ` ] `; +exports[`runs examples > example "chunkInto" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | s:bd speed:2 bank:tr909 ]", + "[ 1/16 → 1/8 | s:sd speed:2 bank:tr909 ]", + "[ 1/8 → 3/16 | s:bd speed:2 bank:tr909 ]", + "[ 3/16 → 1/4 | s:sd speed:2 bank:tr909 ]", + "[ 1/4 → 3/8 | s:ht bank:tr909 ]", + "[ 3/8 → 1/2 | s:lt bank:tr909 ]", + "[ 1/2 → 5/8 | s:bd bank:tr909 ]", + "[ 3/4 → 7/8 | s:cp bank:tr909 ]", + "[ 7/8 → 1/1 | s:lt bank:tr909 ]", + "[ 1/1 → 9/8 | s:bd bank:tr909 ]", + "[ 9/8 → 5/4 | s:sd bank:tr909 ]", + "[ 5/4 → 21/16 | s:ht speed:2 bank:tr909 ]", + "[ 21/16 → 11/8 | s:lt speed:2 bank:tr909 ]", + "[ 11/8 → 23/16 | s:ht speed:2 bank:tr909 ]", + "[ 23/16 → 3/2 | s:lt speed:2 bank:tr909 ]", + "[ 3/2 → 13/8 | s:bd bank:tr909 ]", + "[ 7/4 → 15/8 | s:cp bank:tr909 ]", + "[ 15/8 → 2/1 | s:lt bank:tr909 ]", + "[ 2/1 → 17/8 | s:bd bank:tr909 ]", + "[ 17/8 → 9/4 | s:sd bank:tr909 ]", + "[ 9/4 → 19/8 | s:ht bank:tr909 ]", + "[ 19/8 → 5/2 | s:lt bank:tr909 ]", + "[ 5/2 → 41/16 | s:bd speed:2 bank:tr909 ]", + "[ 21/8 → 43/16 | s:bd speed:2 bank:tr909 ]", + "[ 11/4 → 23/8 | s:cp bank:tr909 ]", + "[ 23/8 → 3/1 | s:lt bank:tr909 ]", + "[ 3/1 → 25/8 | s:bd bank:tr909 ]", + "[ 25/8 → 13/4 | s:sd bank:tr909 ]", + "[ 13/4 → 27/8 | s:ht bank:tr909 ]", + "[ 27/8 → 7/2 | s:lt bank:tr909 ]", + "[ 7/2 → 29/8 | s:bd bank:tr909 ]", + "[ 15/4 → 61/16 | s:cp speed:2 bank:tr909 ]", + "[ 61/16 → 31/8 | s:lt speed:2 bank:tr909 ]", + "[ 31/8 → 63/16 | s:cp speed:2 bank:tr909 ]", + "[ 63/16 → 4/1 | s:lt speed:2 bank:tr909 ]", +] +`; + exports[`runs examples > example "clip" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:c s:piano clip:0.5 ]", @@ -4391,6 +4431,35 @@ exports[`runs examples > example "inside" example index 0 1`] = ` ] `; +exports[`runs examples > example "into" example index 0 1`] = ` +[ + "[ 0/1 → 1/8 | s:bd speed:2 ]", + "[ 1/8 → 1/4 | s:sd speed:2 ]", + "[ 1/4 → 3/8 | s:bd speed:2 ]", + "[ 3/8 → 1/2 | s:sd speed:2 ]", + "[ 1/2 → 3/4 | s:ht ]", + "[ 3/4 → 1/1 | s:lt ]", + "[ 1/1 → 9/8 | s:bd speed:2 ]", + "[ 9/8 → 5/4 | s:sd speed:2 ]", + "[ 5/4 → 11/8 | s:bd speed:2 ]", + "[ 11/8 → 3/2 | s:sd speed:2 ]", + "[ 3/2 → 7/4 | s:ht ]", + "[ 7/4 → 2/1 | s:lt ]", + "[ 2/1 → 17/8 | s:bd speed:2 ]", + "[ 17/8 → 9/4 | s:sd speed:2 ]", + "[ 9/4 → 19/8 | s:bd speed:2 ]", + "[ 19/8 → 5/2 | s:sd speed:2 ]", + "[ 5/2 → 11/4 | s:ht ]", + "[ 11/4 → 3/1 | s:lt ]", + "[ 3/1 → 25/8 | s:bd speed:2 ]", + "[ 25/8 → 13/4 | s:sd speed:2 ]", + "[ 13/4 → 27/8 | s:bd speed:2 ]", + "[ 27/8 → 7/2 | s:sd speed:2 ]", + "[ 7/2 → 15/4 | s:ht ]", + "[ 15/4 → 4/1 | s:lt ]", +] +`; + exports[`runs examples > example "invert" example index 0 1`] = ` [ "[ 0/1 → 1/8 | s:bd ]", From e19fd2444704b1767f415d68c897d3da0058eb6a Mon Sep 17 00:00:00 2001 From: Khalid Date: Wed, 25 Jun 2025 08:44:12 -0400 Subject: [PATCH 162/174] Case insensitive search in the reference tab --- website/src/repl/components/panel/Reference.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/website/src/repl/components/panel/Reference.jsx b/website/src/repl/components/panel/Reference.jsx index ab925eb91..42a03a02e 100644 --- a/website/src/repl/components/panel/Reference.jsx +++ b/website/src/repl/components/panel/Reference.jsx @@ -21,7 +21,8 @@ export function Reference() { return true; } - return entry.name.includes(search) || (entry.synonyms?.some((s) => s.includes(search)) ?? false); + const lowCaseSearch = search.toLowerCase(); + return entry.name.toLowerCase().includes(lowCaseSearch) || (entry.synonyms?.some((s) => s.includes(lowCaseSearch)) ?? false); }); }, [search]); From 82e3f9b837d300e51360181f5c9c5d9a11bc5651 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 26 Jun 2025 14:59:24 +0200 Subject: [PATCH 163/174] fix: js example --- website/src/pages/learn/mondo-notation.mdx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/website/src/pages/learn/mondo-notation.mdx b/website/src/pages/learn/mondo-notation.mdx index ccf5b72b6..3b00b9902 100644 --- a/website/src/pages/learn/mondo-notation.mdx +++ b/website/src/pages/learn/mondo-notation.mdx @@ -117,10 +117,10 @@ Here's the same written in JS: *4") - # scale("C4:minor") - # jux(rev) - # dec(.2) - # delay(.5)`} + .scale("C4:minor") + .jux(rev) + .dec(.2) + .delay(.5)`} /> ### Chaining Functions Locally From b1aba294ad66bd6e8da6147cb474367fe2a187c4 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 26 Jun 2025 15:39:35 +0200 Subject: [PATCH 164/174] hotfix: fix upstream test --- packages/tonal/test/tonal.test.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tonal/test/tonal.test.mjs b/packages/tonal/test/tonal.test.mjs index 226522690..cd8b3c88b 100644 --- a/packages/tonal/test/tonal.test.mjs +++ b/packages/tonal/test/tonal.test.mjs @@ -39,7 +39,7 @@ describe('tonal', () => { }); it('scale without tonic', () => { expect( - n(0, 1, 2) + n(seq(0, 1, 2)) .scale('major') .firstCycleValues.map((h) => h.note), ).toEqual(['C3', 'D3', 'E3']); From 09d05abd707fefb77bdceb3f0d7dee35227e87ba Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Fri, 27 Jun 2025 15:54:39 -0400 Subject: [PATCH 165/174] delaysync --- packages/core/controls.mjs | 18 +++++++++++++++++- packages/superdough/superdough.mjs | 2 +- packages/webaudio/webaudio.mjs | 1 - 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 3a7faf081..c37960d41 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -985,15 +985,31 @@ export const { delayfeedback, delayfb, dfb } = registerControl('delayfeedback', * */ export const { delaytime, delayt, dt } = registerControl('delaytime', 'delayt', 'dt'); -/* // TODO: test + +/** + * Sets the time of the delay effect in cycles. + * + * @name delaysync + * @param {number | Pattern} cycles delay length in cycles + * @synonyms delayt, dt + * @example + * s("bd bd").delay(.25).delaysync("<1 2 3 5>".div(8)) + * + */ +export const { delaysync } = registerControl('delaysync'); + +/** * Specifies whether delaytime is calculated relative to cps. * * @name lock * @param {number | Pattern} enable When set to 1, delaytime is a direct multiple of a cycle. + * @superdirtOnly * @example * s("sd").delay().lock(1).osc() * + * */ + export const { lock } = registerControl('lock'); /** * Set detune for stacked voices of supported oscillators diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index fc75276a8..14f46711b 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -548,7 +548,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5) => { compressorRelease, } = value; - delaytime = delaytime ?? cycleToSeconds(delaysync) + delaytime = delaytime ?? cycleToSeconds(delaysync, cps); const orbitChannels = mapChannelNumbers( multiChannelOrbits && orbit > 0 ? [orbit * 2 - 1, orbit * 2] : getDefaultValue('channels'), diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 67490cd23..91e28defd 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -21,7 +21,6 @@ export const webaudioOutput = (hap, deadline, hapDuration, cps, t) => { return superdough(hap2value(hap), t ? `=${t}` : deadline, hapDuration, cps); }; - Pattern.prototype.webaudio = function () { return this.onTrigger(webaudioOutputTrigger); }; From 7a53f6c021b89d53664a222b0f46b0a17c58a4f4 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Fri, 27 Jun 2025 15:56:37 -0400 Subject: [PATCH 166/174] add test --- test/__snapshots__/examples.test.mjs.snap | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index f3607cb48..ed3b5f793 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -2505,6 +2505,19 @@ exports[`runs examples > example "delayfeedback" example index 0 1`] = ` ] `; +exports[`runs examples > example "delaysync" example index 0 1`] = ` +[ + "[ 0/1 → 1/2 | s:bd delay:0.25 delaysync:0.125 ]", + "[ 1/2 → 1/1 | s:bd delay:0.25 delaysync:0.125 ]", + "[ 1/1 → 3/2 | s:bd delay:0.25 delaysync:0.25 ]", + "[ 3/2 → 2/1 | s:bd delay:0.25 delaysync:0.25 ]", + "[ 2/1 → 5/2 | s:bd delay:0.25 delaysync:0.375 ]", + "[ 5/2 → 3/1 | s:bd delay:0.25 delaysync:0.375 ]", + "[ 3/1 → 7/2 | s:bd delay:0.25 delaysync:0.625 ]", + "[ 7/2 → 4/1 | s:bd delay:0.25 delaysync:0.625 ]", +] +`; + exports[`runs examples > example "delaytime" example index 0 1`] = ` [ "[ 0/1 → 1/2 | s:bd delay:0.25 delaytime:0.125 ]", @@ -5016,6 +5029,15 @@ exports[`runs examples > example "linger" example index 0 1`] = ` ] `; +exports[`runs examples > example "lock" example index 0 1`] = ` +[ + "[ 0/1 → 1/1 | delay:{s:sd} lock:1 ]", + "[ 1/1 → 2/1 | delay:{s:sd} lock:1 ]", + "[ 2/1 → 3/1 | delay:{s:sd} lock:1 ]", + "[ 3/1 → 4/1 | delay:{s:sd} lock:1 ]", +] +`; + exports[`runs examples > example "loop" example index 0 1`] = ` [ "[ 0/1 → 1/1 | s:casio loop:1 ]", From ff5b11f5edee691e5cb126f6a88d0fc4471bc4d0 Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 27 Jun 2025 22:44:15 +0100 Subject: [PATCH 167/174] test + bugfix for chunkinto --- packages/core/pattern.mjs | 36 ++++++++++++++--------------- packages/core/test/pattern.test.mjs | 23 ++++++++++++++++++ 2 files changed, 40 insertions(+), 19 deletions(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 10527a1a8..c8c744628 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -894,22 +894,6 @@ export class Pattern { into(pieces, func) { return this.unjoin(pieces, func).innerJoin(); } - - /** - * Like `chunk`, but the function is applied to a looped subcycle of the source pattern. - * @name chunkInto - * @synonym chunkinto - * @memberof Pattern - * @example - * sound("bd sd ht lt bd - cp lt").chunkInto(4, hurry(2)) - * .bank("tr909") - */ - chunkInto(n, func) { - return this.into(fastcat(true, ...Array(n - 1).fill(false)).iterback(4), func); - } - chunkinto(n, func) { - return this.chunkInto(n, func); - } } ////////////////////////////////////////////////////////////////////// @@ -1858,9 +1842,9 @@ export const { fastGap, fastgap } = register(['fastGap', 'fastgap'], function (f const newWhole = !hap.whole ? undefined : new TimeSpan( - newPart.begin.sub(begin.sub(hap.whole.begin).div(factor)), - newPart.end.add(hap.whole.end.sub(end).div(factor)), - ); + newPart.begin.sub(begin.sub(hap.whole.begin).div(factor)), + newPart.end.add(hap.whole.end.sub(end).div(factor)), + ); return new Hap(newWhole, newPart, hap.value, hap.context); }; return pat.withQuerySpanMaybe(qf).withHap(ef).splitQueries(); @@ -2535,6 +2519,20 @@ export const { fastchunk, fastChunk } = register( true, ); +/** + * Like `chunk`, but the function is applied to a looped subcycle of the source pattern. + * @name chunkInto + * @synonym chunkinto + * @memberof Pattern + * @example + * sound("bd sd ht lt bd - cp lt").chunkInto(4, hurry(2)) + * .bank("tr909") + */ +export const { chunkinto, chunkInto } = register(['chunkinto', 'chunkInto'], + function (n, func, pat) { + return pat.into(fastcat(true, ...Array(n - 1).fill(false)).iterback(n), func); + }); + // TODO - redefine elsewhere in terms of mask export const bypass = register( 'bypass', diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs index 874d45132..8d26a990c 100644 --- a/packages/core/test/pattern.test.mjs +++ b/packages/core/test/pattern.test.mjs @@ -1271,4 +1271,27 @@ describe('Pattern', () => { ); }); }); + describe('unjoin', () => { + it('destructures a pattern into subcycles', () => { + sameFirst( + fastcat('a', 'b', 'c', 'd').unjoin(fastcat(true, fastcat(true, true))).fmap(fast(2)).join(), + fastcat('a', 'b', 'a', 'b', 'c', 'c', 'd', 'd') + ) + }) + }) + describe('into', () => { + it('applies a function to subcycles of a pattern', () => { + sameFirst( + fastcat('a', 'b', 'c', 'd').into(fastcat(fastcat('true', 'true'), 'true'), fast(2)), + fastcat('a', 'a', 'b', 'b', 'c', 'd', 'c', 'd') + ) + }) + }), + describe('chunkinto', () => { + it('chunks into subcycles', () => { + sameFirst(fastcat('a', 'b', 'c').chunkInto(3, fast(2)).fast(3), + fastcat(fastcat('a', 'a'), 'b', 'c', 'a', fastcat('b', 'b'), 'c', 'a', 'b', fastcat('c', 'c')) + ) + }) + }) }); From f44caf90966e34271f155f366721a64b060c1f97 Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 27 Jun 2025 22:51:47 +0100 Subject: [PATCH 168/174] tests and tweaks, and add chunkBackInto --- packages/core/pattern.mjs | 16 +++++++++++++++- packages/core/test/pattern.test.mjs | 23 +++++++++++++++-------- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index c8c744628..6d42c51fd 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -2530,7 +2530,21 @@ export const { fastchunk, fastChunk } = register( */ export const { chunkinto, chunkInto } = register(['chunkinto', 'chunkInto'], function (n, func, pat) { - return pat.into(fastcat(true, ...Array(n - 1).fill(false)).iterback(n), func); + return pat.into(fastcat(true, ...Array(n - 1).fill(false))._iterback(n), func); + }); + +/** + * Like `chunkInto`, but moves backwards through the chunks. + * @name chunkBackInto + * @synonym chunkbackinto + * @memberof Pattern + * @example + * sound("bd sd ht lt bd - cp lt").chunkInto(4, hurry(2)) + * .bank("tr909") + */ +export const { chunkbackinto, chunkBackInto } = register(['chunkbackinto', 'chunkBackInto'], + function (n, func, pat) { + return pat.into(fastcat(true, ...Array(n - 1).fill(false))._iter(n)._early(1), func); }); // TODO - redefine elsewhere in terms of mask diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs index 8d26a990c..4ab519a75 100644 --- a/packages/core/test/pattern.test.mjs +++ b/packages/core/test/pattern.test.mjs @@ -1278,7 +1278,7 @@ describe('Pattern', () => { fastcat('a', 'b', 'a', 'b', 'c', 'c', 'd', 'd') ) }) - }) + }); describe('into', () => { it('applies a function to subcycles of a pattern', () => { sameFirst( @@ -1286,12 +1286,19 @@ describe('Pattern', () => { fastcat('a', 'a', 'b', 'b', 'c', 'd', 'c', 'd') ) }) - }), - describe('chunkinto', () => { - it('chunks into subcycles', () => { - sameFirst(fastcat('a', 'b', 'c').chunkInto(3, fast(2)).fast(3), - fastcat(fastcat('a', 'a'), 'b', 'c', 'a', fastcat('b', 'b'), 'c', 'a', 'b', fastcat('c', 'c')) - ) - }) + }); + describe('chunkinto', () => { + it('chunks into subcycles', () => { + sameFirst(fastcat('a', 'b', 'c').chunkInto(3, fast(2)).fast(3), + fastcat(fastcat('a', 'a'), 'b', 'c', 'a', fastcat('b', 'b'), 'c', 'a', 'b', fastcat('c', 'c')) + ) }) + }); + describe('chunkbackinto', () => { + it('chunks into subcycles backwards', () => { + sameFirst(fastcat('a', 'b', 'c').chunkBackInto(3, fast(2)).fast(3), + fastcat('a', 'b', fastcat('c', 'c'), 'a', fastcat('b', 'b'), 'c', fastcat('a', 'a'), 'b', 'c') + ) + }) + }); }); From 27073d6c17264dd3218102bd72bcb96d6a0186d6 Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 27 Jun 2025 22:56:19 +0100 Subject: [PATCH 169/174] format --- packages/core/pattern.mjs | 25 ++++++++++++--------- packages/core/test/pattern.test.mjs | 35 ++++++++++++++++------------- 2 files changed, 34 insertions(+), 26 deletions(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 6d42c51fd..cbaf8a8a2 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -1842,9 +1842,9 @@ export const { fastGap, fastgap } = register(['fastGap', 'fastgap'], function (f const newWhole = !hap.whole ? undefined : new TimeSpan( - newPart.begin.sub(begin.sub(hap.whole.begin).div(factor)), - newPart.end.add(hap.whole.end.sub(end).div(factor)), - ); + newPart.begin.sub(begin.sub(hap.whole.begin).div(factor)), + newPart.end.add(hap.whole.end.sub(end).div(factor)), + ); return new Hap(newWhole, newPart, hap.value, hap.context); }; return pat.withQuerySpanMaybe(qf).withHap(ef).splitQueries(); @@ -2528,10 +2528,9 @@ export const { fastchunk, fastChunk } = register( * sound("bd sd ht lt bd - cp lt").chunkInto(4, hurry(2)) * .bank("tr909") */ -export const { chunkinto, chunkInto } = register(['chunkinto', 'chunkInto'], - function (n, func, pat) { - return pat.into(fastcat(true, ...Array(n - 1).fill(false))._iterback(n), func); - }); +export const { chunkinto, chunkInto } = register(['chunkinto', 'chunkInto'], function (n, func, pat) { + return pat.into(fastcat(true, ...Array(n - 1).fill(false))._iterback(n), func); +}); /** * Like `chunkInto`, but moves backwards through the chunks. @@ -2542,10 +2541,14 @@ export const { chunkinto, chunkInto } = register(['chunkinto', 'chunkInto'], * sound("bd sd ht lt bd - cp lt").chunkInto(4, hurry(2)) * .bank("tr909") */ -export const { chunkbackinto, chunkBackInto } = register(['chunkbackinto', 'chunkBackInto'], - function (n, func, pat) { - return pat.into(fastcat(true, ...Array(n - 1).fill(false))._iter(n)._early(1), func); - }); +export const { chunkbackinto, chunkBackInto } = register(['chunkbackinto', 'chunkBackInto'], function (n, func, pat) { + return pat.into( + fastcat(true, ...Array(n - 1).fill(false)) + ._iter(n) + ._early(1), + func, + ); +}); // TODO - redefine elsewhere in terms of mask export const bypass = register( diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs index 4ab519a75..93c4168c9 100644 --- a/packages/core/test/pattern.test.mjs +++ b/packages/core/test/pattern.test.mjs @@ -1274,31 +1274,36 @@ describe('Pattern', () => { describe('unjoin', () => { it('destructures a pattern into subcycles', () => { sameFirst( - fastcat('a', 'b', 'c', 'd').unjoin(fastcat(true, fastcat(true, true))).fmap(fast(2)).join(), - fastcat('a', 'b', 'a', 'b', 'c', 'c', 'd', 'd') - ) - }) + fastcat('a', 'b', 'c', 'd') + .unjoin(fastcat(true, fastcat(true, true))) + .fmap(fast(2)) + .join(), + fastcat('a', 'b', 'a', 'b', 'c', 'c', 'd', 'd'), + ); + }); }); describe('into', () => { it('applies a function to subcycles of a pattern', () => { sameFirst( fastcat('a', 'b', 'c', 'd').into(fastcat(fastcat('true', 'true'), 'true'), fast(2)), - fastcat('a', 'a', 'b', 'b', 'c', 'd', 'c', 'd') - ) - }) + fastcat('a', 'a', 'b', 'b', 'c', 'd', 'c', 'd'), + ); + }); }); describe('chunkinto', () => { it('chunks into subcycles', () => { - sameFirst(fastcat('a', 'b', 'c').chunkInto(3, fast(2)).fast(3), - fastcat(fastcat('a', 'a'), 'b', 'c', 'a', fastcat('b', 'b'), 'c', 'a', 'b', fastcat('c', 'c')) - ) - }) + sameFirst( + fastcat('a', 'b', 'c').chunkInto(3, fast(2)).fast(3), + fastcat(fastcat('a', 'a'), 'b', 'c', 'a', fastcat('b', 'b'), 'c', 'a', 'b', fastcat('c', 'c')), + ); + }); }); describe('chunkbackinto', () => { it('chunks into subcycles backwards', () => { - sameFirst(fastcat('a', 'b', 'c').chunkBackInto(3, fast(2)).fast(3), - fastcat('a', 'b', fastcat('c', 'c'), 'a', fastcat('b', 'b'), 'c', fastcat('a', 'a'), 'b', 'c') - ) - }) + sameFirst( + fastcat('a', 'b', 'c').chunkBackInto(3, fast(2)).fast(3), + fastcat('a', 'b', fastcat('c', 'c'), 'a', fastcat('b', 'b'), 'c', fastcat('a', 'a'), 'b', 'c'), + ); + }); }); }); From df06248c54709e68d39d4f4ae607af4509742842 Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 27 Jun 2025 22:59:01 +0100 Subject: [PATCH 170/174] snapshot --- test/__snapshots__/examples.test.mjs.snap | 40 +++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index d0a622940..2d20cff4c 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -1889,6 +1889,46 @@ exports[`runs examples > example "chunkBack" example index 0 1`] = ` ] `; +exports[`runs examples > example "chunkBackInto" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | s:bd speed:2 bank:tr909 ]", + "[ 1/16 → 1/8 | s:sd speed:2 bank:tr909 ]", + "[ 1/8 → 3/16 | s:bd speed:2 bank:tr909 ]", + "[ 3/16 → 1/4 | s:sd speed:2 bank:tr909 ]", + "[ 1/4 → 3/8 | s:ht bank:tr909 ]", + "[ 3/8 → 1/2 | s:lt bank:tr909 ]", + "[ 1/2 → 5/8 | s:bd bank:tr909 ]", + "[ 3/4 → 7/8 | s:cp bank:tr909 ]", + "[ 7/8 → 1/1 | s:lt bank:tr909 ]", + "[ 1/1 → 9/8 | s:bd bank:tr909 ]", + "[ 9/8 → 5/4 | s:sd bank:tr909 ]", + "[ 5/4 → 21/16 | s:ht speed:2 bank:tr909 ]", + "[ 21/16 → 11/8 | s:lt speed:2 bank:tr909 ]", + "[ 11/8 → 23/16 | s:ht speed:2 bank:tr909 ]", + "[ 23/16 → 3/2 | s:lt speed:2 bank:tr909 ]", + "[ 3/2 → 13/8 | s:bd bank:tr909 ]", + "[ 7/4 → 15/8 | s:cp bank:tr909 ]", + "[ 15/8 → 2/1 | s:lt bank:tr909 ]", + "[ 2/1 → 17/8 | s:bd bank:tr909 ]", + "[ 17/8 → 9/4 | s:sd bank:tr909 ]", + "[ 9/4 → 19/8 | s:ht bank:tr909 ]", + "[ 19/8 → 5/2 | s:lt bank:tr909 ]", + "[ 5/2 → 41/16 | s:bd speed:2 bank:tr909 ]", + "[ 21/8 → 43/16 | s:bd speed:2 bank:tr909 ]", + "[ 11/4 → 23/8 | s:cp bank:tr909 ]", + "[ 23/8 → 3/1 | s:lt bank:tr909 ]", + "[ 3/1 → 25/8 | s:bd bank:tr909 ]", + "[ 25/8 → 13/4 | s:sd bank:tr909 ]", + "[ 13/4 → 27/8 | s:ht bank:tr909 ]", + "[ 27/8 → 7/2 | s:lt bank:tr909 ]", + "[ 7/2 → 29/8 | s:bd bank:tr909 ]", + "[ 15/4 → 61/16 | s:cp speed:2 bank:tr909 ]", + "[ 61/16 → 31/8 | s:lt speed:2 bank:tr909 ]", + "[ 31/8 → 63/16 | s:cp speed:2 bank:tr909 ]", + "[ 63/16 → 4/1 | s:lt speed:2 bank:tr909 ]", +] +`; + exports[`runs examples > example "chunkInto" example index 0 1`] = ` [ "[ 0/1 → 1/16 | s:bd speed:2 bank:tr909 ]", From 77bab433e0c897048e874ad97f0721733a55948e Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sat, 28 Jun 2025 14:06:16 -0400 Subject: [PATCH 171/174] cf --- website/src/repl/components/panel/Reference.jsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/website/src/repl/components/panel/Reference.jsx b/website/src/repl/components/panel/Reference.jsx index 42a03a02e..1b617341b 100644 --- a/website/src/repl/components/panel/Reference.jsx +++ b/website/src/repl/components/panel/Reference.jsx @@ -21,8 +21,11 @@ export function Reference() { return true; } - const lowCaseSearch = search.toLowerCase(); - return entry.name.toLowerCase().includes(lowCaseSearch) || (entry.synonyms?.some((s) => s.includes(lowCaseSearch)) ?? false); + const lowCaseSearch = search.toLowerCase(); + return ( + entry.name.toLowerCase().includes(lowCaseSearch) || + (entry.synonyms?.some((s) => s.includes(lowCaseSearch)) ?? false) + ); }); }, [search]); From f2330a2307fcdba42b55fe976635dce3e941cfcc Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 29 Jun 2025 20:18:15 +0200 Subject: [PATCH 172/174] add 1.1.0 release notes --- .../blog/release-1.1.0-bananensplit.mdx | 317 ++++++++++++++++++ 1 file changed, 317 insertions(+) create mode 100644 website/src/content/blog/release-1.1.0-bananensplit.mdx diff --git a/website/src/content/blog/release-1.1.0-bananensplit.mdx b/website/src/content/blog/release-1.1.0-bananensplit.mdx new file mode 100644 index 000000000..dcc547682 --- /dev/null +++ b/website/src/content/blog/release-1.1.0-bananensplit.mdx @@ -0,0 +1,317 @@ +--- +title: 'Release Notes v1.1.0' +description: '' +date: '2024-06-02' +tags: ['meta'] +author: froos +--- + +These are the release notes for Strudel 1.1.0 aka "Bananensplit". + +The last release was over 19 weeks ago, so a lot of things have happened! + +First, here's a little demo, teasing some of the new features: + +import { Youtube } from '@src/components/Youtube'; + + + +Let's write up some of the highlights: + +## New DSP Features + +### Stereo Supersaw + +with spread, unison, and detune parameters + +```plaintext +note("d f a a# a d3").fast(2) +.s("supersaw").spread(".8").detune(.3).unison("2 7") +``` + +### Analog "ladder" filter type + +works great for acid basslines and vibey tones + +```plaintext +note("{d d d a a# d3 f4}%16".sub(12)).gain(1).s("sawtooth") +.lpf(200).lpenv(slider(1.36,0,8)).lpq(7).distort("1.5:.7")` +.ftype('ladder') +``` + +### stereo distortion effect + +```plaintext +note("{g g a# g g4}%8".add("{0 7 12 0}%8")).lpf(500) +.s("supersaw").dist("4:.2") +``` + +## Editor Features + +### inline viz + +The editor now supports multiple visuals within the code, using the `_` prefix for viz functions: + +Screenshot 2024-06-01 at 01 23 51 + +- `._pianoroll()`: inline pianoroll +- `._punchcard()`: inline punchcard +- `._scope()`: inline scope +- `._pitchwheel()`: inline pitchwheel + +For more info, check out the new [Visual Feedback Page](https://strudel.cc/learn/visual-feedback/) + +### label notation + +This new notation simplifies writing patterns at the top level: + +```plaintext +d1: s("bd*4") +d2: s("[- hh]*4") +``` + +This is equivalent to: + +```plaintext +stack( + s("bd*4"), + s("[- hh]*4") +) +``` + +The labels you choose are arbitrary, the above `d1` and `d2` are a typical thing you'd write in tidal, for example `d1 $ s "bd*4"`. +If the same label is used multiple times, the last one wins: + +```plaintext +d1: s("bd*4") +d1: s("[- hh]*4") // <-- only this plays +``` + +There is a special label anonymous label `$`, which can appear multiple times without overriding itself: + +```plaintext +// both of these will play: +$: s("bd*4") +$: s("[- hh]*4") +``` + +You can mute a pattern by prefixing `_`: + +```plaintext +_$: s("bd*4") // <-- this one is muted +$: s("[- hh]*4") +``` + +To run a transformation on all patterns, you can use `all`: + +```plaintext +$: s("bd*4") +$: s("[- hh]*4") +all(x=>x.room(.5)) +``` + +This notation is now the recommended way to [play patterns in parallel](https://strudel.cc/workshop/first-notes/#playing-multiple-patterns) + +### Clock sync between multiple instances + +timing has received a major overhaul, and is now much more accurate on all browsers. Additionally, you can now sync timing across multiple windows. +![Screenshot 2024-06-02 at 11 24 40 PM](https://codeberg.org/uzu/strudel/assets/47068718/840be744-a13e-4d7b-ab09-50d3a70b1f85) + +### Better sample upload support + +you can now upload large amounts of samples much faster across all browsers including on IOS devices. supported filetypes now include: ogg flac mp3 wav aac m4a + +### experimental tidal syntax + +The new `tidal` function allows you to write strudel patterns in tidal syntax: + +```plaintext +await initTidal() + +tidal` +d1 $ s "bd*4" + +d2 $ s "[- hh]*4" +` +``` + +As we're looking to improve compatibility with tidal, we're happy to hear feedback. + +## breaking changes + +This release comes with a bunch of breaking changes. If you find your patterns to sound different, check out the PRs below for guidance on how to update them. Most of these changes shouldn't affect a lot of patterns. +In case of doubt, add the line `// @version 1.0` to your old pattern. +If you're having problems, please let us know! + +- remove legacy legato + duration implementations by @felixroos in https://codeberg.org/uzu/strudel/pull/965 +- Velocity in value by @felixroos in https://codeberg.org/uzu/strudel/pull/974 +- use ireal as default voicing dict by @felixroos https://codeberg.org/uzu/strudel/pull/967 +- Color in hap value by @felixroos https://codeberg.org/uzu/strudel/pull/1007 +- rename trig -> reset, trigzero -> restart by @felixroos https://codeberg.org/uzu/strudel/pull/1010 +- remove dangerous arithmetic feature by @felixroos https://codeberg.org/uzu/strudel/pull/1030 +- change fanchor to 0 by @daslyfe https://codeberg.org/uzu/strudel/pull/1107 + +## superdough features + +- replace shape with distort in learn doc by @daslyfe https://codeberg.org/uzu/strudel/pull/982 +- Worklet Improvents / fixes by @daslyfe https://codeberg.org/uzu/strudel/pull/963 +- supersaw oscillator by @daslyfe https://codeberg.org/uzu/strudel/pull/978 +- Add analog-style ladder filter by @daslyfe https://codeberg.org/uzu/strudel/pull/1103 +- Calculate phaser modulation phase based on time by @daslyfe https://codeberg.org/uzu/strudel/pull/1110 +- rollback phaser by @daslyfe https://codeberg.org/uzu/strudel/pull/1113 + +## editor / ui features + +- 'Enable Bracket Matching' option in Codemirror by @eefano https://codeberg.org/uzu/strudel/pull/956 +- REPL sync between windows by @daslyfe https://codeberg.org/uzu/strudel/pull/900 +- inline viz / widgets package by @felixroos https://codeberg.org/uzu/strudel/pull/989 +- Inline punchcard + spiral by @felixroos https://codeberg.org/uzu/strudel/pull/1008 +- More fonts by @felixroos https://codeberg.org/uzu/strudel/pull/1023 +- better theme integration for visuals + various fixes by @felixroos https://codeberg.org/uzu/strudel/pull/1024 +- add setting for sync flag by @felixroos https://codeberg.org/uzu/strudel/pull/1025 +- add closeBrackets setting by @felixroos https://codeberg.org/uzu/strudel/pull/1031 +- add font file types to offline cache by @felixroos https://codeberg.org/uzu/strudel/pull/1032 +- pitchwheel visual by @felixroos https://codeberg.org/uzu/strudel/pull/1041 +- repl: set document.title from @title by @kasparsj https://codeberg.org/uzu/strudel/pull/1090 +- Samples tab improvements by @daslyfe https://codeberg.org/uzu/strudel/pull/1102 + +## language features + +- pickOut(), pickRestart(), pickReset() by @eefano https://codeberg.org/uzu/strudel/pull/950 +- Auto await samples by @felixroos https://codeberg.org/uzu/strudel/pull/955 +- feat: can now invert euclid pulses with negative numbers by @felixroos https://codeberg.org/uzu/strudel/pull/959 +- Nested controls by @felixroos https://codeberg.org/uzu/strudel/pull/973 +- alias - for ~ by @yaxu https://codeberg.org/uzu/strudel/pull/981 +- Beat-oriented functionality by @yaxu https://codeberg.org/uzu/strudel/pull/976 +- Labeled statements by @felixroos https://codeberg.org/uzu/strudel/pull/991 +- accidentals in scale degrees by @eefano https://codeberg.org/uzu/strudel/pull/1000 +- Feature: tactus marking by @yaxu https://codeberg.org/uzu/strudel/pull/1021 +- Tactus tidy by @yaxu https://codeberg.org/uzu/strudel/pull/1027 +- Wax, wane, taper and taperlist by @yaxu https://codeberg.org/uzu/strudel/pull/1042 +- transpose: support all combinations of numbers and strings for notes and intervals by @felixroos https://codeberg.org/uzu/strudel/pull/1048 +- anonymous patterns + muting by @felixroos https://codeberg.org/uzu/strudel/pull/1059 +- add swing + swingBy by @felixroos https://codeberg.org/uzu/strudel/pull/1038 +- Stepwise functions from Tidal by @yaxu https://codeberg.org/uzu/strudel/pull/1060 +- Tactus tweaks - fixes for maintaining tactus and highlight locations by @yaxu https://codeberg.org/uzu/strudel/pull/1065 +- Fix stepjoin by @yaxu https://codeberg.org/uzu/strudel/pull/1067 +- More tactus tidying by @yaxu https://codeberg.org/uzu/strudel/pull/1071 +- Tactus calculation toggle and breaking change to tactus calculation in fast/slow/hurry by @yaxu https://codeberg.org/uzu/strudel/pull/1081 +- hs2js package / tidal parser by @felixroos https://codeberg.org/uzu/strudel/pull/870 +- Add the mousex and mousey signal by @Enelg52 https://codeberg.org/uzu/strudel/pull/1112 +- can now access strudelMirror from repl by @felixroos https://codeberg.org/uzu/strudel/pull/1117 + +## sampler + +If you have nodejs installed on your system, you can now use [@strudel/sampler](https://www.npmjs.com/package/@strudel/sampler) to serve samples from disk to the REPL or flok. + +- local sample server cli by @felixroos https://codeberg.org/uzu/strudel/pull/1033 +- Fix sampler paths by @felixroos https://codeberg.org/uzu/strudel/pull/1034 +- Fix sampler windows by @felixroos https://codeberg.org/uzu/strudel/pull/1108 +- fix sampler on windows by @geikha https://codeberg.org/uzu/strudel/pull/1109 + +## docs + +- V1 release notes by @felixroos https://codeberg.org/uzu/strudel/pull/935 +- Minor documentation error: Update first-sounds.mdx by @mhetrick https://codeberg.org/uzu/strudel/pull/941 +- Update synths.mdx by @andresgottlieb https://codeberg.org/uzu/strudel/pull/984 +- using strudel in your project guide + cleanup examples by @felixroos https://codeberg.org/uzu/strudel/pull/1006 +- Document signals by @ilesinge https://codeberg.org/uzu/strudel/pull/1015 +- improve tutorial + custom samples doc by @felixroos https://codeberg.org/uzu/strudel/pull/1053 +- fix cr typo on first-sounds.mdx by @cleary https://codeberg.org/uzu/strudel/pull/1068 +- fix first sounds typo by @cleary https://codeberg.org/uzu/strudel/pull/1069 +- add `<...>` to first-sounds.mdx recap by @cleary https://codeberg.org/uzu/strudel/pull/1070 +- add nesting to `off` example variation in pattern-effects.mdx by @cleary https://codeberg.org/uzu/strudel/pull/1075 +- fix translation issue in first-effects.mdx by @cleary https://codeberg.org/uzu/strudel/pull/1072 +- add signals to recap in first-effects.mdx by @cleary https://codeberg.org/uzu/strudel/pull/1073 +- fix docs on alignment.mdx by @diegodorado https://codeberg.org/uzu/strudel/pull/1076 +- fix little dub tune example by @lukad https://codeberg.org/uzu/strudel/pull/1104 +- clarify `off` in pattern-effects.mdx by @cleary https://codeberg.org/uzu/strudel/pull/1074 +- Fixes drawPianoroll import in codemirror example by @giohappy https://codeberg.org/uzu/strudel/pull/1116 +- Migrate tutorial fanchor by @felixroos https://codeberg.org/uzu/strudel/pull/1122 + +## internals + +- remove cjs builds by @felixroos https://codeberg.org/uzu/strudel/pull/945 +- controls refactoring: simplify exports by @felixroos https://codeberg.org/uzu/strudel/pull/962 +- move canvas related helpers from core to new draw package by @felixroos https://codeberg.org/uzu/strudel/pull/971 +- remove canvas, externalize samples, delete junk by @felixroos https://codeberg.org/uzu/strudel/pull/1003 +- Improve performance of ! (replicate) by @yaxu https://codeberg.org/uzu/strudel/pull/1084 +- Benchmarks by @yaxu https://codeberg.org/uzu/strudel/pull/1079 + +## fixes + +- fix midi issue on firefox and added quote error by @Enelg52 https://codeberg.org/uzu/strudel/pull/936 +- fix: pianoroll sorting by @felixroos https://codeberg.org/uzu/strudel/pull/938 +- account for cps in midi time duration by @daslyfe https://codeberg.org/uzu/strudel/pull/954 +- fix script importable packages (web + repl) by @felixroos https://codeberg.org/uzu/strudel/pull/957 +- fix: reset global fx on pattern change by @felixroos https://codeberg.org/uzu/strudel/pull/960 +- add debounce to logger by @felixroos https://codeberg.org/uzu/strudel/pull/968 +- fix for transpose(): preserve hap value object structure by @eefano https://codeberg.org/uzu/strudel/pull/966 +- fix: clear hydra on reset by @felixroos https://codeberg.org/uzu/strudel/pull/983 +- little fix for withVal by @eefano https://codeberg.org/uzu/strudel/pull/980 +- fix: share now shares what's visible instead of active by @felixroos https://codeberg.org/uzu/strudel/pull/985 +- Fix pure mini highlight by @yaxu https://codeberg.org/uzu/strudel/pull/994 +- fix: await injectPatternMethods by @felixroos https://codeberg.org/uzu/strudel/pull/1012 +- update undocumented script by @felixroos https://codeberg.org/uzu/strudel/pull/1013 +- eliminate chromium clock jitter by @felixroos https://codeberg.org/uzu/strudel/pull/1004 +- Repl sync fixes by @daslyfe https://codeberg.org/uzu/strudel/pull/1014 +- hotfix for 1017 by @daslyfe https://codeberg.org/uzu/strudel/pull/1020 +- fix cyclist fizzling out by @felixroos https://codeberg.org/uzu/strudel/pull/1046 +- Midi Time hotfix for scheduler updates by @daslyfe https://codeberg.org/uzu/strudel/pull/1047 +- fix: do not reset cc input values on each eval by @felixroos https://codeberg.org/uzu/strudel/pull/1054 +- Fix wchooseCycles not picking the whole pattern by @ilesinge https://codeberg.org/uzu/strudel/pull/1061 +- fix OSC timing for recent scheduler updates by @daslyfe https://codeberg.org/uzu/strudel/pull/1062 +- clarify license by @yaxu https://codeberg.org/uzu/strudel/pull/1064 +- fix failing format test by @daslyfe https://codeberg.org/uzu/strudel/pull/1077 +- fix: url parsing with extra params by @felixroos https://codeberg.org/uzu/strudel/pull/1083 +- fix: csound + dough timing by @felixroos https://codeberg.org/uzu/strudel/pull/1086 +- fix: missing events due to premature worklet cleanup by @felixroos https://codeberg.org/uzu/strudel/pull/1089 +- Use sessionStorage for viewingPatternData and activePattern by @kasparsj https://codeberg.org/uzu/strudel/pull/1091 +- osc: couple of fixes by @kasparsj https://codeberg.org/uzu/strudel/pull/1093 +- web package fixes by @felixroos https://codeberg.org/uzu/strudel/pull/1044 +- Fix audio worklets by @daslyfe https://codeberg.org/uzu/strudel/pull/1114 +- fix: use full repl in web package by @felixroos https://codeberg.org/uzu/strudel/pull/1119 +- [BUG FIX] Audio worklets sometimes dont load by @daslyfe https://codeberg.org/uzu/strudel/pull/1121 + +## New Contributors + +- @mhetrick made their first contribution https://codeberg.org/uzu/strudel/pull/941 +- @eefano made their first contribution https://codeberg.org/uzu/strudel/pull/956 +- @Enelg52 made their first contribution https://codeberg.org/uzu/strudel/pull/936 +- @andresgottlieb made their first contribution https://codeberg.org/uzu/strudel/pull/984 +- @cleary made their first contribution https://codeberg.org/uzu/strudel/pull/1068 +- @diegodorado made their first contribution https://codeberg.org/uzu/strudel/pull/1076 +- @lukad made their first contribution https://codeberg.org/uzu/strudel/pull/1104 +- @giohappy made their first contribution https://codeberg.org/uzu/strudel/pull/1116 + +A huge thanks to all contributors!!! + +## Packages + +- @strudel/codemirror@1.1.0 +- @strudel/core@1.1.0 +- @strudel/csound@1.1.0 +- @strudel/draw@1.1.0 +- @strudel/embed@1.1.0 +- hs2js@0.1.0 +- @strudel/hydra@1.1.0 +- @strudel/midi@1.1.0 +- @strudel/mini@1.1.0 +- @strudel/osc@1.1.0 +- @strudel/repl@1.1.0 +- @strudel/sampler@0.1.0 +- @strudel/serial@1.1.0 +- @strudel/soundfonts@1.1.0 +- superdough@1.1.0 +- @strudel/tidal@0.1.0 +- @strudel/tonal@1.1.0 +- @strudel/transpiler@1.1.0 +- @strudel/web@1.1.0 +- @strudel/webaudio@1.1.0 +- @strudel/xen@1.1.0 + +**Full Changelog**: https://codeberg.org/uzu/strudel/compare/v1.0.0...v1.1.0 From eac6f27ca4e79a0b21c2c48e6cc3bdf3331e1be5 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 29 Jun 2025 21:29:11 +0200 Subject: [PATCH 173/174] add 1.2.0 release notes --- .../blog/release-1.2.0-kardinalschnitten.mdx | 187 ++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 website/src/content/blog/release-1.2.0-kardinalschnitten.mdx diff --git a/website/src/content/blog/release-1.2.0-kardinalschnitten.mdx b/website/src/content/blog/release-1.2.0-kardinalschnitten.mdx new file mode 100644 index 000000000..e58e27aec --- /dev/null +++ b/website/src/content/blog/release-1.2.0-kardinalschnitten.mdx @@ -0,0 +1,187 @@ +--- +title: 'Release Notes v1.2.0' +description: '' +date: '2025-05-01' +tags: ['meta'] +author: froos +--- + +## What's Changed + +## highlights + +- [stepwise functions](https://strudel.cc/learn/stepwise/) ([PR](https://github.com/tidalcycles/strudel/pull/1262)) +- [midimaps](https://strudel.cc/learn/input-output/#midimaps) ([PR](https://github.com/tidalcycles/strudel/pull/1274)) +- [spectrum](https://strudel.cc/learn/visual-feedback/#spectrum) ([PR](https://github.com/tidalcycles/strudel/pull/1213)) +- [mqtt](https://strudel.cc/learn/input-output/#mqtt) ([PR](https://github.com/tidalcycles/strudel/pull/1224)) +- pulse oscillator (todo: https://github.com/tidalcycles/strudel/issues/1336) ([PR](https://github.com/tidalcycles/strudel/pull/1304)) +- theme improvements + +## breaking changes + +- [breaking change] Sample signals from query onset, rather than midpoint by @yaxu in https://github.com/tidalcycles/strudel/pull/1278 +- change behaviour of polymeter, and remove polymeterSteps by @yaxu in https://github.com/tidalcycles/strudel/pull/1302 +- Polish, rename, and document stepwise functions by @yaxu in https://github.com/tidalcycles/strudel/pull/1262 + +### superdough + +- feat: Create Pulse Oscillator with variable PWM by @daslyfe in https://github.com/tidalcycles/strudel/pull/1304 +- add num samples (edited numbers) by @yaxu in https://github.com/tidalcycles/strudel/pull/1309 +- Add num samples from 0 up to 20 by @yaxu in https://github.com/tidalcycles/strudel/pull/1310 +- feat: add max polyphony feature for superdough by @daslyfe in https://github.com/tidalcycles/strudel/pull/1317 + +### docs + +- doc: visual functions + refactor onPaint by @felixroos in https://github.com/tidalcycles/strudel/pull/1125 +- Labeled statements doc by @felixroos in https://github.com/tidalcycles/strudel/pull/1126 +- Correct spelling mistakes by @EdwardBetts in https://github.com/tidalcycles/strudel/pull/1183 +- remove redundant example for cat, update snapshot by @kdiab in https://github.com/tidalcycles/strudel/pull/1189 +- chore: Edit run locally instructions in README.md by @ChinoUkaegbu in https://github.com/tidalcycles/strudel/pull/1206 +- suggested changes to voicings.mdx by @bwagner in https://github.com/tidalcycles/strudel/pull/1231 +- Documentation for all/each, and bugfix for each by @yaxu in https://github.com/tidalcycles/strudel/pull/1233 +- Update documentation for param value modification by @gillespi314 in https://github.com/tidalcycles/strudel/pull/1238 +- fix docs for beat function by @daslyfe in https://github.com/tidalcycles/strudel/pull/1248 +- understand voicings page by @felixroos in https://github.com/tidalcycles/strudel/pull/1230 +- add reference package by @felixroos in https://github.com/tidalcycles/strudel/pull/1252 +- Stepwise documentation tweaks, with mridangam samples by @yaxu in https://github.com/tidalcycles/strudel/pull/1275 +- showcase tweaks by @yaxu in https://github.com/tidalcycles/strudel/pull/1291 +- Signpost licenses for source code and samples a bit more, ref #1277 by @yaxu in https://github.com/tidalcycles/strudel/pull/1289 +- Fix misplaced ending sentence by @makmanalp in https://github.com/tidalcycles/strudel/pull/1296 +- Fix typo pattnr by @ReneNyffenegger in https://github.com/tidalcycles/strudel/pull/1316 +- update docs to reflect import sounds tab change by @hpunq in https://github.com/tidalcycles/strudel/pull/1332 + +### ui improvements + +- Udels (MultiFrame Strudel) Revisited by @daslyfe in https://github.com/tidalcycles/strudel/pull/1132 +- Create audio target selector for OSC/Superdirt by @daslyfe in https://github.com/tidalcycles/strudel/pull/1160 +- Add a search bar to the REPL Reference tab by @netux in https://github.com/tidalcycles/strudel/pull/1165 +- Adding search bar (soundtab.jsx) by @Bubobubobubobubo in https://github.com/tidalcycles/strudel/pull/1185 +- add 2 new ui settings by @felixroos in https://github.com/tidalcycles/strudel/pull/1200 +- Theme glowup by @felixroos in https://github.com/tidalcycles/strudel/pull/1268 +- Create Pattern Page Pagination by @daslyfe in https://github.com/tidalcycles/strudel/pull/1287 +- feat: Theme improvements by @daslyfe in https://github.com/tidalcycles/strudel/pull/1295 +- feat: new themes + theme improvements by @daslyfe in https://github.com/tidalcycles/strudel/pull/1326 +- Add new "import-sounds" tab with explanation on folder import by @hpunq in https://github.com/tidalcycles/strudel/pull/1329 +- Add Icon to import sample button by @daslyfe in https://github.com/tidalcycles/strudel/pull/1331 +- better spacing in zen mode by @felixroos in https://github.com/tidalcycles/strudel/pull/1147 +- Screenreader improvements by @yaxu in https://github.com/tidalcycles/strudel/pull/1158 +- colorize console + tweak header by @felixroos in https://github.com/tidalcycles/strudel/pull/1203 +- Menu Panel Improvements! by @daslyfe in https://github.com/tidalcycles/strudel/pull/1193 +- Make panel hover behavior optional by @daslyfe in https://github.com/tidalcycles/strudel/pull/1199 +- REPL: solo and sync configuration by @bthj in https://github.com/tidalcycles/strudel/pull/1214 +- enhancement: make error messages easier to read by @daslyfe in https://github.com/tidalcycles/strudel/pull/1315 + +### mqtt + +- MQTT support by @yaxu in https://github.com/tidalcycles/strudel/pull/1224 +- MQTT - if password isn't provided, prompt for one by @yaxu in https://github.com/tidalcycles/strudel/pull/1249 +- MQTT - support adding hap duration and cps metadata to JSON messages by @yaxu in https://github.com/tidalcycles/strudel/pull/1279 +- make mqtt topic patternable by @yaxu in https://github.com/tidalcycles/strudel/pull/1280 +- Bugfix: update mqtt connections dictionary by @yaxu in https://github.com/tidalcycles/strudel/pull/1281 +- mqtt bugfix - connection check by @yaxu in https://github.com/tidalcycles/strudel/pull/1282 + +### new functions + +- Add scramble and shuffle by @yaxu in https://github.com/tidalcycles/strudel/pull/1167 +- polyJoin by @yaxu in https://github.com/tidalcycles/strudel/pull/1168 +- Add seqPLoop from Tidal by @yaxu in https://github.com/tidalcycles/strudel/pull/1182 +- add filter + filterWhen + within by @felixroos in https://github.com/tidalcycles/strudel/pull/1039 +- Add bite function by @yaxu in https://github.com/tidalcycles/strudel/pull/1187 +- markcss by @felixroos in https://github.com/tidalcycles/strudel/pull/1202 +- "beat" function for "step sequencer" style rhythm notation by @daslyfe in https://github.com/tidalcycles/strudel/pull/1237 +- Add s_zip for 'cat'-ing patterns together step-by-step, bugfix `steps` by @yaxu in https://github.com/tidalcycles/strudel/pull/1208 +- "stretch" function (phase vocoder) by @daslyfe in https://github.com/tidalcycles/strudel/pull/1130 +- add basic spectrum function by @felixroos in https://github.com/tidalcycles/strudel/pull/1213 +- Add onKey function for custom key commands for patterns by @daslyfe in https://github.com/tidalcycles/strudel/pull/1235 +- Add binary and binaryN by @heerman in https://github.com/tidalcycles/strudel/pull/1226 +- midimaps by @felixroos in https://github.com/tidalcycles/strudel/pull/1274 +- small feat: Add alias for segment and ribbon by @daslyfe in https://github.com/tidalcycles/strudel/pull/1314 +- feat: Create scrub function for scrubbing an audio file by @daslyfe in https://github.com/tidalcycles/strudel/pull/1321 +- feat: Improve gain curve by @daslyfe in https://github.com/tidalcycles/strudel/pull/1318 +- Chop chop by @yaxu in https://github.com/tidalcycles/strudel/pull/1078 + +### more + +- Make `all()` post-stack again, and add `each()` for pre-stack by @yaxu in https://github.com/tidalcycles/strudel/pull/1229 +- Add stepBind, and some toplevel aliases for binds and withValue by @yaxu in https://github.com/tidalcycles/strudel/pull/1241 +- Make cps patternable by @eefano in https://github.com/tidalcycles/strudel/pull/1001 +- Allow wchooseCycles probabilities to be patterned by @yaxu in https://github.com/tidalcycles/strudel/pull/1292 +- @strudel/sampler improvements by @felixroos in https://github.com/tidalcycles/strudel/pull/1288 + +### refactor + +- export comment commands by @felixroos in https://github.com/tidalcycles/strudel/pull/113 + 6 +- containerize/seperate out boolean checks for repl types/Repl logic into bespoke components. by @daslyfe in https://github.com/tidalcycles/strudel/pull/1163 +- Improve + simplify neocyclist timing by @daslyfe in https://github.com/tidalcycles/strudel/pull/1164 +- Make phaser control consistent with superdirt by @daslyfe in https://github.com/tidalcycles/strudel/pull/1178 +- Revert "Make phaser control consistent with superdirt" by @daslyfe in https://github.com/tidalcycles/strudel/pull/1179 +- make phaser control match superdirt by @daslyfe in https://github.com/tidalcycles/strudel/pull/1180 +- refactor sampler by @felixroos in https://github.com/tidalcycles/strudel/pull/1101 +- update lockfile + minor versions by @felixroos in https://github.com/tidalcycles/strudel/pull/1198 +- Preserve tactus for 'degrade' and friends, and tidy up 'pick' and friends by @yaxu in https://github.com/tidalcycles/strudel/pull/1205 +- Apply `all` function to individual patterns rather than final stack by @yaxu in https://github.com/tidalcycles/strudel/pull/1209 +- Revert "Fix sometimes" by @yaxu in https://github.com/tidalcycles/strudel/pull/1267 +- patchday by @felixroos in https://github.com/tidalcycles/strudel/pull/1264 +- Rename repeat back to extend by @yaxu in https://github.com/tidalcycles/strudel/pull/1285 +- Send delta in OSC message in seconds, to match tidal/superdirt by @yaxu in https://github.com/tidalcycles/strudel/pull/1323 + +### fixes + +- Fix clock worker dependency path in module builds by @matthewkaney in https://github.com/tidalcycles/strudel/pull/1129 +- Fix bug in Fraction.lcm by @yaxu in https://github.com/tidalcycles/strudel/pull/1133 +- Fix tactus marking in mininotation by @yaxu in https://github.com/tidalcycles/strudel/pull/1144 +- Fix loopAt tactus by @yaxu in https://github.com/tidalcycles/strudel/pull/1145 +- Fix OSC clock jitter by @daslyfe in https://github.com/tidalcycles/strudel/pull/1157 +- [CORS HOTFIX] by @daslyfe in https://github.com/tidalcycles/strudel/pull/1162 +- Fixes fit so it works after a chop or slice by @yaxu in https://github.com/tidalcycles/strudel/pull/1171 +- fix sample speed when using splice and fit with superdirt by @daslyfe in https://github.com/tidalcycles/strudel/pull/1172 +- handle midin device not found error by @felixroos in https://github.com/tidalcycles/strudel/pull/1146 +- Fix serial timing by @yaxu in https://github.com/tidalcycles/strudel/pull/1188 +- Fix regression for d1, p1, p(n) by @yaxu in https://github.com/tidalcycles/strudel/pull/1227 +- Fix sometimes by @yaxu in https://github.com/tidalcycles/strudel/pull/1243 +- Fix sf2 timing by @felixroos in https://github.com/tidalcycles/strudel/pull/1272 +- Fix "squeezejoin" and functions using it, including "bite" by @yaxu in https://github.com/tidalcycles/strudel/pull/1286 +- Fixes inverted triangle wave by renaming it to "itri", making non-inverted "tri" by @yaxu in https://github.com/tidalcycles/strudel/pull/1283 +- Hotfix: prevent undefined pattern code from crashing strudel on load by @daslyfe in https://github.com/tidalcycles/strudel/pull/1297 +- Fix test error #1297 by @nkymut in https://github.com/tidalcycles/strudel/pull/1298 +- bugfix zoom stepcount by @yaxu in https://github.com/tidalcycles/strudel/pull/1301 +- bugfix: Allow single param to be used in the as function by @daslyfe in https://github.com/tidalcycles/strudel/pull/1312 +- fix: replace empty spaces in registered sound keys by @daslyfe in https://github.com/tidalcycles/strudel/pull/1319 +- FIX: Multichannel Audio by @daslyfe in https://github.com/tidalcycles/strudel/pull/1322 +- fix: udels header by @daslyfe in https://github.com/tidalcycles/strudel/pull/1325 +- fix: disable astro toolbar by default by @daslyfe in https://github.com/tidalcycles/strudel/pull/1324 +- FIX: sound import order by @daslyfe in https://github.com/tidalcycles/strudel/pull/1333` + +## New Contributors + +- @EdwardBetts made their first contribution in https://github.com/tidalcycles/strudel/pull/1183 +- @netux made their first contribution in https://github.com/tidalcycles/strudel/pull/1165 +- @kdiab made their first contribution in https://github.com/tidalcycles/strudel/pull/1189 + +**Full Changelog**: https://github.com/tidalcycles/strudel/compare/v1.1.0...v1.1.1 + +## packages + +- @strudel/codemirror@1.2.0 +- @strudel/core@1.2.0 +- @strudel/csound@1.2.0 +- @strudel/draw@1.2.0 +- @strudel/gamepad@1.2.0 +- @strudel/hydra@1.2.0 +- @strudel/midi@1.2.0 +- @strudel/mini@1.2.0 +- @strudel/motion@1.2.0 +- @strudel/mqtt@1.2.0 +- @strudel/osc@1.2.0 +- @strudel/reference@1.2.0 +- @strudel/repl@1.2.0 +- @strudel/sampler@0.2.0 +- @strudel/serial@1.2.0 +- @strudel/soundfonts@1.2.0 +- superdough@1.2.0 +- @strudel/tonal@1.2.0 +- @strudel/transpiler@1.2.0 +- @strudel/web@1.2.0 +- @strudel/webaudio@1.2.0 +- @strudel/xen@1.2.0 From 5a72ea94b15e1905e82dd210ac18f63ebae9b5cc Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 30 Jun 2025 05:15:30 +0200 Subject: [PATCH 174/174] fix: format --- website/src/content/blog/release-1.2.0-kardinalschnitten.mdx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/website/src/content/blog/release-1.2.0-kardinalschnitten.mdx b/website/src/content/blog/release-1.2.0-kardinalschnitten.mdx index e58e27aec..e215d4532 100644 --- a/website/src/content/blog/release-1.2.0-kardinalschnitten.mdx +++ b/website/src/content/blog/release-1.2.0-kardinalschnitten.mdx @@ -110,8 +110,7 @@ author: froos ### refactor -- export comment commands by @felixroos in https://github.com/tidalcycles/strudel/pull/113 - 6 +- expose comment commands by @felixroos in https://github.com/tidalcycles/strudel/pull/1136 - containerize/seperate out boolean checks for repl types/Repl logic into bespoke components. by @daslyfe in https://github.com/tidalcycles/strudel/pull/1163 - Improve + simplify neocyclist timing by @daslyfe in https://github.com/tidalcycles/strudel/pull/1164 - Make phaser control consistent with superdirt by @daslyfe in https://github.com/tidalcycles/strudel/pull/1178