diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7813cf60..0f7d0ead 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,10 +10,12 @@ jobs: node-version: [18] steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v3 - with: - node-version: ${{ matrix.node-version }} - cache: 'npm' - - run: npm install - - run: npm test + - uses: actions/checkout@v2 + - uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + cache: "npm" + - run: npm install + - run: npm run format-check + - run: npm run lint + - run: npm test diff --git a/.prettierignore b/.prettierignore index b4980cfc..d962cf6b 100644 --- a/.prettierignore +++ b/.prettierignore @@ -5,3 +5,4 @@ **/out **/dist packages/mini/krill-parser.js +packages/xen/tunejs.js diff --git a/.prettierrc b/.prettierrc index 12500f0d..c35a73b5 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,16 +1,15 @@ { - "printWidth": 120, - "useTabs": false, - "tabWidth": 2, - "semi": true, - "singleQuote": true, - "jsxSingleQuote": false, - "trailingComma": "all", - "bracketSpacing": true, - "jsxBracketSameLine": false, - "arrowParens": "always", - "proseWrap": "preserve", - "htmlWhitespaceSensitivity": "css", - "endOfLine": "lf" - } - \ No newline at end of file + "printWidth": 120, + "useTabs": false, + "tabWidth": 2, + "semi": true, + "singleQuote": true, + "jsxSingleQuote": false, + "trailingComma": "all", + "bracketSpacing": true, + "jsxBracketSameLine": false, + "arrowParens": "always", + "proseWrap": "preserve", + "htmlWhitespaceSensitivity": "css", + "endOfLine": "lf" +} diff --git a/package-lock.json b/package-lock.json index 504504be..ad4cb98b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@strudel.cycles/monorepo", - "version": "0.0.4", + "version": "0.5.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@strudel.cycles/monorepo", - "version": "0.0.4", + "version": "0.5.0", "license": "AGPL-3.0-or-later", "workspaces": [ "packages/*" @@ -12044,7 +12044,7 @@ }, "packages/core": { "name": "@strudel.cycles/core", - "version": "0.4.1", + "version": "0.5.0", "license": "AGPL-3.0-or-later", "dependencies": { "bjork": "^0.0.1", @@ -12053,7 +12053,7 @@ }, "packages/csound": { "name": "@strudel.cycles/csound", - "version": "0.3.0", + "version": "0.5.1", "license": "AGPL-3.0-or-later", "dependencies": { "@csound/browser": "^6.18.3" @@ -12066,10 +12066,10 @@ }, "packages/eval": { "name": "@strudel.cycles/eval", - "version": "0.4.1", + "version": "0.5.0", "license": "AGPL-3.0-or-later", "dependencies": { - "@strudel.cycles/core": "^0.4.1", + "@strudel.cycles/core": "^0.5.0", "estraverse": "^5.3.0", "shift-ast": "^7.0.0", "shift-codegen": "^8.1.0", @@ -12080,10 +12080,10 @@ }, "packages/midi": { "name": "@strudel.cycles/midi", - "version": "0.4.1", + "version": "0.5.0", "license": "AGPL-3.0-or-later", "dependencies": { - "@strudel.cycles/tone": "^0.4.1", + "@strudel.cycles/tone": "^0.5.0", "tone": "^14.7.77", "webmidi": "^3.0.21" } @@ -12103,12 +12103,12 @@ }, "packages/mini": { "name": "@strudel.cycles/mini", - "version": "0.4.1", + "version": "0.5.0", "license": "AGPL-3.0-or-later", "dependencies": { - "@strudel.cycles/core": "^0.4.1", - "@strudel.cycles/eval": "^0.4.1", - "@strudel.cycles/tone": "^0.4.1" + "@strudel.cycles/core": "^0.5.0", + "@strudel.cycles/eval": "^0.5.0", + "@strudel.cycles/tone": "^0.5.0" }, "devDependencies": { "peggy": "^2.0.1" @@ -12116,7 +12116,7 @@ }, "packages/osc": { "name": "@strudel.cycles/osc", - "version": "0.3.1", + "version": "0.4.0", "license": "AGPL-3.0-or-later", "dependencies": { "osc-js": "^2.4.0" @@ -12127,14 +12127,14 @@ }, "packages/react": { "name": "@strudel.cycles/react", - "version": "0.4.2", + "version": "0.5.0", "license": "AGPL-3.0-or-later", "dependencies": { "@codemirror/lang-javascript": "^6.1.1", - "@strudel.cycles/core": "^0.4.1", - "@strudel.cycles/tone": "^0.4.1", - "@strudel.cycles/transpiler": "^0.4.1", - "@strudel.cycles/webaudio": "^0.4.2", + "@strudel.cycles/core": "^0.5.0", + "@strudel.cycles/tone": "^0.5.0", + "@strudel.cycles/transpiler": "^0.5.0", + "@strudel.cycles/webaudio": "^0.5.0", "@uiw/codemirror-themes": "^4.12.4", "@uiw/react-codemirror": "^4.12.4", "react-hook-inview": "^4.5.0" @@ -12201,16 +12201,16 @@ }, "packages/serial": { "name": "@strudel.cycles/serial", - "version": "0.2.0", + "version": "0.3.0", "license": "AGPL-3.0-or-later" }, "packages/soundfonts": { "name": "@strudel.cycles/soundfonts", - "version": "0.4.2", + "version": "0.5.0", "license": "AGPL-3.0-or-later", "dependencies": { - "@strudel.cycles/core": "^0.4.1", - "@strudel.cycles/webaudio": "^0.4.2", + "@strudel.cycles/core": "^0.5.0", + "@strudel.cycles/webaudio": "^0.5.0", "sfumato": "^0.1.2", "soundfont2": "^0.4.0" }, @@ -12237,10 +12237,10 @@ }, "packages/tonal": { "name": "@strudel.cycles/tonal", - "version": "0.4.1", + "version": "0.5.0", "license": "AGPL-3.0-or-later", "dependencies": { - "@strudel.cycles/core": "^0.4.1", + "@strudel.cycles/core": "^0.5.0", "@tonaljs/tonal": "^4.7.2", "chord-voicings": "^0.0.1", "webmidi": "^3.0.21" @@ -12261,19 +12261,19 @@ }, "packages/tone": { "name": "@strudel.cycles/tone", - "version": "0.4.1", + "version": "0.5.0", "license": "AGPL-3.0-or-later", "dependencies": { - "@strudel.cycles/core": "^0.4.1", + "@strudel.cycles/core": "^0.5.0", "tone": "^14.7.77" } }, "packages/transpiler": { "name": "@strudel.cycles/transpiler", - "version": "0.4.1", + "version": "0.5.0", "license": "AGPL-3.0-or-later", "dependencies": { - "@strudel.cycles/core": "^0.4.1", + "@strudel.cycles/core": "^0.5.0", "acorn": "^8.8.1", "escodegen": "^2.0.0", "estree-walker": "^3.0.1" @@ -12291,27 +12291,27 @@ }, "packages/webaudio": { "name": "@strudel.cycles/webaudio", - "version": "0.4.2", + "version": "0.5.0", "license": "AGPL-3.0-or-later", "dependencies": { - "@strudel.cycles/core": "^0.4.1" + "@strudel.cycles/core": "^0.5.0" } }, "packages/webdirt": { "name": "@strudel.cycles/webdirt", - "version": "0.4.1", + "version": "0.5.0", "license": "AGPL-3.0-or-later", "dependencies": { - "@strudel.cycles/core": "^0.4.1", + "@strudel.cycles/core": "^0.5.0", "WebDirt": "github:dktr0/WebDirt" } }, "packages/xen": { "name": "@strudel.cycles/xen", - "version": "0.4.1", + "version": "0.5.0", "license": "AGPL-3.0-or-later", "dependencies": { - "@strudel.cycles/core": "^0.4.1" + "@strudel.cycles/core": "^0.5.0" } } }, @@ -13740,7 +13740,7 @@ "@strudel.cycles/eval": { "version": "file:packages/eval", "requires": { - "@strudel.cycles/core": "^0.4.1", + "@strudel.cycles/core": "^0.5.0", "estraverse": "^5.3.0", "shift-ast": "^7.0.0", "shift-codegen": "^8.1.0", @@ -13752,7 +13752,7 @@ "@strudel.cycles/midi": { "version": "file:packages/midi", "requires": { - "@strudel.cycles/tone": "^0.4.1", + "@strudel.cycles/tone": "^0.5.0", "tone": "^14.7.77", "webmidi": "^3.0.21" }, @@ -13769,9 +13769,9 @@ "@strudel.cycles/mini": { "version": "file:packages/mini", "requires": { - "@strudel.cycles/core": "^0.4.1", - "@strudel.cycles/eval": "^0.4.1", - "@strudel.cycles/tone": "^0.4.1", + "@strudel.cycles/core": "^0.5.0", + "@strudel.cycles/eval": "^0.5.0", + "@strudel.cycles/tone": "^0.5.0", "peggy": "^2.0.1" } }, @@ -13786,10 +13786,10 @@ "version": "file:packages/react", "requires": { "@codemirror/lang-javascript": "^6.1.1", - "@strudel.cycles/core": "^0.4.1", - "@strudel.cycles/tone": "^0.4.1", - "@strudel.cycles/transpiler": "^0.4.1", - "@strudel.cycles/webaudio": "^0.4.2", + "@strudel.cycles/core": "^0.5.0", + "@strudel.cycles/tone": "^0.5.0", + "@strudel.cycles/transpiler": "^0.5.0", + "@strudel.cycles/webaudio": "^0.5.0", "@types/react": "^17.0.2", "@types/react-dom": "^17.0.2", "@uiw/codemirror-themes": "^4.12.4", @@ -13838,8 +13838,8 @@ "@strudel.cycles/soundfonts": { "version": "file:packages/soundfonts", "requires": { - "@strudel.cycles/core": "^0.4.1", - "@strudel.cycles/webaudio": "^0.4.2", + "@strudel.cycles/core": "^0.5.0", + "@strudel.cycles/webaudio": "^0.5.0", "node-fetch": "^3.2.6", "sfumato": "^0.1.2", "soundfont2": "^0.4.0" @@ -13859,7 +13859,7 @@ "@strudel.cycles/tonal": { "version": "file:packages/tonal", "requires": { - "@strudel.cycles/core": "^0.4.1", + "@strudel.cycles/core": "^0.5.0", "@tonaljs/tonal": "^4.7.2", "chord-voicings": "^0.0.1", "webmidi": "^3.0.21" @@ -13877,14 +13877,14 @@ "@strudel.cycles/tone": { "version": "file:packages/tone", "requires": { - "@strudel.cycles/core": "^0.4.1", + "@strudel.cycles/core": "^0.5.0", "tone": "^14.7.77" } }, "@strudel.cycles/transpiler": { "version": "file:packages/transpiler", "requires": { - "@strudel.cycles/core": "^0.4.1", + "@strudel.cycles/core": "^0.5.0", "acorn": "^8.8.1", "escodegen": "^2.0.0", "estree-walker": "^3.0.1" @@ -13898,20 +13898,20 @@ "@strudel.cycles/webaudio": { "version": "file:packages/webaudio", "requires": { - "@strudel.cycles/core": "^0.4.1" + "@strudel.cycles/core": "^0.5.0" } }, "@strudel.cycles/webdirt": { "version": "file:packages/webdirt", "requires": { - "@strudel.cycles/core": "^0.4.1", + "@strudel.cycles/core": "^0.5.0", "WebDirt": "github:dktr0/WebDirt" } }, "@strudel.cycles/xen": { "version": "file:packages/xen", "requires": { - "@strudel.cycles/core": "^0.4.1" + "@strudel.cycles/core": "^0.5.0" } }, "@tonaljs/abc-notation": { @@ -18656,10 +18656,10 @@ "version": "file:packages/react", "requires": { "@codemirror/lang-javascript": "^6.1.1", - "@strudel.cycles/core": "^0.4.1", - "@strudel.cycles/tone": "^0.4.1", - "@strudel.cycles/transpiler": "^0.4.1", - "@strudel.cycles/webaudio": "^0.4.2", + "@strudel.cycles/core": "^0.5.0", + "@strudel.cycles/tone": "^0.5.0", + "@strudel.cycles/transpiler": "^0.5.0", + "@strudel.cycles/webaudio": "^0.5.0", "@types/react": "^17.0.2", "@types/react-dom": "^17.0.2", "@uiw/codemirror-themes": "^4.12.4", diff --git a/package.json b/package.json index 6921fafc..5841995d 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,11 @@ { "name": "@strudel.cycles/monorepo", - "version": "0.0.4", + "version": "0.5.0", "private": true, "description": "Port of tidalcycles to javascript", "scripts": { "pretest": "cd tutorial && npm run jsdoc-json", - "test": "npm run lint && vitest run --version", + "test": "vitest run --version", "test-ui": "vitest --ui", "test-coverage": "vitest --coverage", "bootstrap": "lerna bootstrap", @@ -18,8 +18,10 @@ "deploy": "NODE_DEBUG=gh-pages gh-pages -d out", "jsdoc": "jsdoc packages/ -c jsdoc.config.json", "jsdoc-json": "jsdoc packages/ --template ./node_modules/jsdoc-json --destination doc.json -c jsdoc.config.json", - "lint": "npx eslint . --ext mjs,js --quiet", - "codeformat": "prettier --write ." + "lint": "eslint . --ext mjs,js --quiet", + "codeformat": "prettier --write .", + "format-check": "prettier --check .", + "check": "npm run format-check && npm run lint && npm run test" }, "workspaces": [ "packages/*" diff --git a/packages/core/examples/vite-vanilla-repl/main.js b/packages/core/examples/vite-vanilla-repl/main.js index 72d80b92..3724ef95 100644 --- a/packages/core/examples/vite-vanilla-repl/main.js +++ b/packages/core/examples/vite-vanilla-repl/main.js @@ -16,7 +16,7 @@ evalScope( import('@strudel.cycles/tonal'), ); -setStringParser(mini) +setStringParser(mini); const { evaluate } = repl({ defaultOutput: webaudioOutput, diff --git a/packages/core/hap.mjs b/packages/core/hap.mjs index ae6b2177..30251959 100644 --- a/packages/core/hap.mjs +++ b/packages/core/hap.mjs @@ -80,16 +80,16 @@ export class Hap { } show(compact = false) { - const value = typeof this.value === 'object' - ? compact + const value = + typeof this.value === 'object' + ? compact ? JSON.stringify(this.value).slice(1, -1).replaceAll('"', '').replaceAll(',', ' ') : JSON.stringify(this.value) - : this.value + : this.value; var spans = ''; if (this.whole == undefined) { spans = '~' + this.part.show; - } - else { + } else { var is_whole = this.whole.begin.equals(this.part.begin) && this.whole.end.equals(this.part.end); if (!this.whole.begin.equals(this.part.begin)) { spans = this.whole.begin.show() + ' ⇜ '; @@ -102,12 +102,10 @@ export class Hap { spans += ')'; } if (!this.whole.end.equals(this.part.end)) { - spans += ' ⇝ ' + this.whole.end.show() + spans += ' ⇝ ' + this.whole.end.show(); } } - return ( - '[ ' + spans + ' | ' + value + ' ]' - ); + return '[ ' + spans + ' | ' + value + ' ]'; } showWhole(compact = false) { diff --git a/packages/core/package-lock.json b/packages/core/package-lock.json index 065394ee..fadffd93 100644 --- a/packages/core/package-lock.json +++ b/packages/core/package-lock.json @@ -1,6 +1,6 @@ { "name": "@strudel.cycles/core", - "version": "0.4.1", + "version": "0.5.0", "lockfileVersion": 2, "requires": true, "packages": { diff --git a/packages/core/package.json b/packages/core/package.json index 04d1b14f..b0a16c85 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@strudel.cycles/core", - "version": "0.4.1", + "version": "0.5.0", "description": "Port of Tidal Cycles to JavaScript", "main": "index.mjs", "type": "module", diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 226be24f..98bc23a7 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -534,6 +534,57 @@ export class Pattern { return this.filterHaps((hap) => hap.whole); } + /** + * Combines adjacent haps with the same value and whole. Only + * intended for use in tests. + */ + defragmentHaps() { + // remove continuous haps + const pat = this.discreteOnly(); + + return pat.withHaps((haps) => { + const result = []; + for (var i = 0; i < haps.length; ++i) { + var searching = true; + var a = haps[i]; + while (searching) { + const a_value = JSON.stringify(haps[i].value); + var found = false; + + for (var j = i + 1; j < haps.length; j++) { + const b = haps[j]; + + if (a.whole.equals(b.whole)) { + if (a.part.begin.eq(b.part.end)) { + if (a_value === JSON.stringify(b.value)) { + // eat the matching hap into 'a' + a = new Hap(a.whole, new TimeSpan(b.part.begin, a.part.end), a.value); + haps.splice(j, 1); + // restart the search + found = true; + break; + } + } else if (b.part.begin.eq(a.part.end)) { + if (a_value == JSON.stringify(b.value)) { + // eat the matching hap into 'a' + a = new Hap(a.whole, new TimeSpan(a.part.begin, b.part.end), a.value); + haps.splice(j, 1); + // restart the search + found = true; + break; + } + } + } + } + + searching = found; + } + result.push(a); + } + return result; + }); + } + /** * Queries the pattern for the first cycle, returning Haps. Mainly of use when * debugging a pattern. diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs index 589c18e3..993b5292 100644 --- a/packages/core/test/pattern.test.mjs +++ b/packages/core/test/pattern.test.mjs @@ -915,4 +915,20 @@ describe('Pattern', () => { expect(sequence(1, 2).add.squeeze(4, 5).firstCycle()).toStrictEqual(sequence(5, 6, 6, 7).firstCycle()); }); }); + describe('defragmentHaps', () => { + it('Can merge two touching haps with same whole and value', () => { + expect(stack(pure('a').mask(1, 0), pure('a').mask(0, 1)).defragmentHaps().firstCycle().length).toStrictEqual(1); + }); + it('Doesnt merge two overlapping haps', () => { + expect(stack(pure('a').mask(1, 1, 0), pure('a').mask(0, 1)).defragmentHaps().firstCycle().length).toStrictEqual( + 2, + ); + }); + it('Doesnt merge two touching haps with different values', () => { + expect(stack(pure('a').mask(1, 0), pure('b').mask(0, 1)).defragmentHaps().firstCycle().length).toStrictEqual(2); + }); + it('Doesnt merge two touching haps with different wholes', () => { + expect(stack(sequence('a', silence), pure('a').mask(0, 1)).defragmentHaps().firstCycle().length).toStrictEqual(2); + }); + }); }); diff --git a/packages/core/test/util.test.mjs b/packages/core/test/util.test.mjs index 17dceb81..9ebccee3 100644 --- a/packages/core/test/util.test.mjs +++ b/packages/core/test/util.test.mjs @@ -10,6 +10,7 @@ import { tokenizeNote, toMidi, fromMidi, + freqToMidi, _mod, compose, getFrequency, @@ -94,6 +95,12 @@ describe('fromMidi', () => { expect(fromMidi(57)).toEqual(220); }); }); +describe('freqToMidi', () => { + it('should turn frequency into midi', () => { + expect(freqToMidi(440)).toEqual(69); + expect(freqToMidi(220)).toEqual(57); + }); +}); describe('getFrequency', () => { const happify = (val, context = {}) => pure(val).firstCycle()[0].setContext(context); it('should turn note into frequency', () => { diff --git a/packages/core/timespan.mjs b/packages/core/timespan.mjs index c2a1651e..4cbfb999 100644 --- a/packages/core/timespan.mjs +++ b/packages/core/timespan.mjs @@ -20,9 +20,9 @@ export class TimeSpan { // Support zero-width timespans if (begin.equals(end)) { - return([new TimeSpan(begin, end)]); + return [new TimeSpan(begin, end)]; } - + while (end.gt(begin)) { // If begin and end are in the same cycle, we're done. if (begin.sam().equals(end_sam)) { diff --git a/packages/core/util.mjs b/packages/core/util.mjs index b1997977..d8a7a233 100644 --- a/packages/core/util.mjs +++ b/packages/core/util.mjs @@ -31,6 +31,30 @@ export const fromMidi = (n) => { return Math.pow(2, (n - 69) / 12) * 440; }; +export const freqToMidi = (freq) => { + return (12 * Math.log(freq / 440)) / Math.LN2 + 69; +}; + +export const valueToMidi = (value, fallbackValue) => { + if (typeof value !== 'object') { + throw new Error('valueToMidi: expected object value'); + } + let { freq, note } = value; + if (typeof freq === 'number') { + return freqToMidi(freq); + } + if (typeof note === 'string') { + return toMidi(note); + } + if (typeof note === 'number') { + return note; + } + if (!fallbackValue) { + throw new Error('valueToMidi: expected freq or note to be set'); + } + return fallbackValue; +}; + /** * @deprecated does not appear to be referenced or invoked anywhere in the codebase */ diff --git a/packages/csound/csound.mjs b/packages/csound/csound.mjs index 7b0dfba9..b9e26391 100644 --- a/packages/csound/csound.mjs +++ b/packages/csound/csound.mjs @@ -79,21 +79,25 @@ function eventLogger(type, args) { async function load() { if (window.__csound__) { - // allows using some other csound instance + // Allows using some other csound instance. + // In that case, the external Csound is responsible + // for compiling an orchestra and starting to perform. + logger('[load] Using external Csound', 'warning'); _csound = window.__csound__; + return _csound; } else { const { Csound } = await import('@csound/browser'); _csound = await Csound({ audioContext: getAudioContext() }); + _csound.removeAllListeners('message'); + ['message'].forEach((k) => _csound.on(k, (...args) => eventLogger(k, args))); + await _csound.setOption('-m0d'); // see -m flag https://csound.com/docs/manual/CommandFlags.html + await _csound.setOption('--sample-accurate'); + await _csound.compileCsdText(csd); + // await _csound.compileOrc(livecodeOrc); + await _csound.compileOrc(presetsOrc); + await _csound.start(); + return _csound; } - _csound.removeAllListeners('message'); - ['message'].forEach((k) => _csound.on(k, (...args) => eventLogger(k, args))); - await _csound.setOption('-m0d'); // see -m flag https://csound.com/docs/manual/CommandFlags.html - await _csound.setOption('--sample-accurate'); - await _csound.compileCsdText(csd); - // await _csound.compileOrc(livecodeOrc); - await _csound.compileOrc(presetsOrc); - await _csound.start(); - return _csound; } async function init() { @@ -118,3 +122,49 @@ export async function loadOrc(url) { } await orcCache[url]; } + +/** + * Sends notes to Csound for rendering with MIDI semantics. The hap value is + * translated to these Csound pfields: + * + * p1 -- Csound instrument either as a number (1-based, can be a fraction), + * or as a string name. + * p2 -- time in beats (usually seconds) from start of performance. + * p3 -- duration in beats (usually seconds). + * p4 -- MIDI key number (as a real number, not an integer but in [0, 127]. + * p5 -- MIDI velocity (as a real number, not an integer but in [0, 127]. + * p6 -- Strudel controls, as a string. + */ +export const csoundm = register('csoundm', (instrument, pat) => { + let p1 = instrument; + if (typeof instrument === 'string') { + p1 = `"{instrument}"`; + } + init(); // not async to support csound inside other patterns + to be able to call pattern methods after it + return pat.onTrigger((tidal_time, hap) => { + if (!_csound) { + logger('[csound] not loaded yet', 'warning'); + return; + } + if (typeof hap.value !== 'object') { + throw new Error('csound only support objects as hap values'); + } + // Time in seconds counting from now. + const p2 = tidal_time - getAudioContext().currentTime; + const p3 = hap.duration.valueOf() + 0; + const frequency = getFrequency(hap); + // Translate frequency to MIDI key number _without_ rounding. + const C4 = 261.62558; + let octave = Math.log(frequency / C4) / Math.log(2.0) + 8.0; + const p4 = octave * 12.0 - 36.0; + // We prefer floating point precision, but over the MIDI range [0, 127]. + const p5 = 127 * (hap.context?.velocity ?? 0.9); + // The Strudel controls as a string. + const p6 = Object.entries({ ...hap.value, frequency }) + .flat() + .join('/'); + const i_statement = `i ${p1} ${p2} ${p3} ${p4} ${p5} "${p6}"`; + console.log('[csoundm]:', i_statement); + _csound.inputMessage(i_statement); + }); +}); diff --git a/packages/csound/package.json b/packages/csound/package.json index 90a2f7d4..321da4eb 100644 --- a/packages/csound/package.json +++ b/packages/csound/package.json @@ -1,6 +1,6 @@ { "name": "@strudel.cycles/csound", - "version": "0.3.0", + "version": "0.5.1", "description": "csound bindings for strudel", "main": "csound.mjs", "scripts": { diff --git a/packages/eval/package-lock.json b/packages/eval/package-lock.json index b551e545..99131fd0 100644 --- a/packages/eval/package-lock.json +++ b/packages/eval/package-lock.json @@ -1,6 +1,6 @@ { "name": "@strudel.cycles/eval", - "version": "0.4.1", + "version": "0.5.0", "lockfileVersion": 2, "requires": true, "packages": { diff --git a/packages/eval/package.json b/packages/eval/package.json index 7316ef2a..506cc77d 100644 --- a/packages/eval/package.json +++ b/packages/eval/package.json @@ -1,6 +1,6 @@ { "name": "@strudel.cycles/eval", - "version": "0.4.1", + "version": "0.5.0", "description": "Code evaluator for strudel", "main": "index.mjs", "type": "module", @@ -28,7 +28,7 @@ }, "homepage": "https://github.com/tidalcycles/strudel#readme", "dependencies": { - "@strudel.cycles/core": "^0.4.1", + "@strudel.cycles/core": "^0.5.0", "estraverse": "^5.3.0", "shift-ast": "^7.0.0", "shift-codegen": "^8.1.0", diff --git a/packages/midi/package-lock.json b/packages/midi/package-lock.json index a1dc944f..3fd16a55 100644 --- a/packages/midi/package-lock.json +++ b/packages/midi/package-lock.json @@ -1,6 +1,6 @@ { "name": "@strudel.cycles/midi", - "version": "0.4.1", + "version": "0.5.0", "lockfileVersion": 2, "requires": true, "packages": { diff --git a/packages/midi/package.json b/packages/midi/package.json index 88f95ae6..56db895f 100644 --- a/packages/midi/package.json +++ b/packages/midi/package.json @@ -1,6 +1,6 @@ { "name": "@strudel.cycles/midi", - "version": "0.4.1", + "version": "0.5.0", "description": "Midi API for strudel", "main": "index.mjs", "repository": { @@ -21,7 +21,7 @@ }, "homepage": "https://github.com/tidalcycles/strudel#readme", "dependencies": { - "@strudel.cycles/tone": "^0.4.1", + "@strudel.cycles/tone": "^0.5.0", "tone": "^14.7.77", "webmidi": "^3.0.21" } diff --git a/packages/mini/mini.mjs b/packages/mini/mini.mjs index 26609613..00ecb24a 100644 --- a/packages/mini/mini.mjs +++ b/packages/mini/mini.mjs @@ -6,7 +6,6 @@ This program is free software: you can redistribute it and/or modify it under th import * as krill from './krill-parser.js'; import * as strudel from '@strudel.cycles/core'; -// import { addMiniLocations } from '@strudel.cycles/eval/shapeshifter.mjs'; const { pure, Fraction, stack, slowcat, sequence, timeCat, silence, reify } = strudel; @@ -104,11 +103,11 @@ function resolveReplications(ast) { }); } -export function patternifyAST(ast) { +export function patternifyAST(ast, code) { switch (ast.type_) { case 'pattern': { resolveReplications(ast); - const children = ast.source_.map(patternifyAST).map(applyOptions(ast)); + const children = ast.source_.map((child) => patternifyAST(child, code)).map(applyOptions(ast)); const alignment = ast.arguments_.alignment; if (alignment === 'v') { return stack(...children); @@ -137,9 +136,6 @@ export function patternifyAST(ast) { return silence; } if (typeof ast.source_ !== 'object') { - /* if (!addMiniLocations) { - return ast.source_; - } */ if (!ast.location_) { console.warn('no location for', ast); return ast.source_; @@ -148,12 +144,20 @@ export function patternifyAST(ast) { const value = !isNaN(Number(ast.source_)) ? Number(ast.source_) : ast.source_; // the following line expects the shapeshifter append .withMiniLocation // because location_ is only relative to the mini string, but we need it relative to whole code - return pure(value).withLocation([start.line, start.column, start.offset], [end.line, end.column, end.offset]); + // make sure whitespaces are not part of the highlight: + const actual = code?.split('').slice(start.offset, end.offset).join(''); + const [offsetStart = 0, offsetEnd = 0] = actual + ? actual.split(ast.source_).map((p) => p.split('').filter((c) => c === ' ').length) + : []; + return pure(value).withLocation( + [start.line, start.column + offsetStart, start.offset + offsetStart], + [start.line, end.column - offsetEnd, end.offset - offsetEnd], + ); } - return patternifyAST(ast.source_); + return patternifyAST(ast.source_, code); } case 'stretch': - return patternifyAST(ast.source_).slow(ast.arguments_.amount); + return patternifyAST(ast.source_, code).slow(ast.arguments_.amount); /* case 'scale': let [tonic, scale] = Scale.tokenize(ast.arguments_.scale); const intervals = Scale.get(scale).intervals; @@ -185,8 +189,9 @@ export function patternifyAST(ast) { // mini notation only (wraps in "") export const mini = (...strings) => { const pats = strings.map((str) => { - const ast = krill.parse(`"${str}"`); - return patternifyAST(ast); + const code = `"${str}"`; + const ast = krill.parse(code); + return patternifyAST(ast, code); }); return sequence(...pats); }; @@ -195,7 +200,7 @@ export const mini = (...strings) => { export const h = (string) => { const ast = krill.parse(string); // console.log('ast', ast); - return patternifyAST(ast); + return patternifyAST(ast, string); }; export function minify(thing) { diff --git a/packages/mini/package-lock.json b/packages/mini/package-lock.json index 5d2d9c95..5e6edb33 100644 --- a/packages/mini/package-lock.json +++ b/packages/mini/package-lock.json @@ -1,6 +1,6 @@ { "name": "@strudel.cycles/mini", - "version": "0.4.1", + "version": "0.5.0", "lockfileVersion": 2, "requires": true, "packages": { diff --git a/packages/mini/package.json b/packages/mini/package.json index 744949da..342116de 100644 --- a/packages/mini/package.json +++ b/packages/mini/package.json @@ -1,6 +1,6 @@ { "name": "@strudel.cycles/mini", - "version": "0.4.1", + "version": "0.5.0", "description": "Mini notation for strudel", "main": "index.mjs", "type": "module", @@ -26,9 +26,9 @@ }, "homepage": "https://github.com/tidalcycles/strudel#readme", "dependencies": { - "@strudel.cycles/core": "^0.4.1", - "@strudel.cycles/eval": "^0.4.1", - "@strudel.cycles/tone": "^0.4.1" + "@strudel.cycles/core": "^0.5.0", + "@strudel.cycles/eval": "^0.5.0", + "@strudel.cycles/tone": "^0.5.0" }, "devDependencies": { "peggy": "^2.0.1" diff --git a/packages/osc/package-lock.json b/packages/osc/package-lock.json index ecfe4131..69efff3c 100644 --- a/packages/osc/package-lock.json +++ b/packages/osc/package-lock.json @@ -1,6 +1,6 @@ { "name": "@strudel.cycles/osc", - "version": "0.3.1", + "version": "0.4.0", "lockfileVersion": 2, "requires": true, "packages": { diff --git a/packages/osc/package.json b/packages/osc/package.json index 3b04b02f..e79d1613 100644 --- a/packages/osc/package.json +++ b/packages/osc/package.json @@ -1,6 +1,6 @@ { "name": "@strudel.cycles/osc", - "version": "0.3.1", + "version": "0.4.0", "description": "OSC messaging for strudel", "main": "osc.mjs", "scripts": { diff --git a/packages/osc/server.js b/packages/osc/server.js index ba6f9f90..42722d28 100644 --- a/packages/osc/server.js +++ b/packages/osc/server.js @@ -25,8 +25,6 @@ const config = { }, }; - - const osc = new OSC({ plugin: new OSC.BridgePlugin(config) }); osc.open(); // start a WebSocket server on port 8080 diff --git a/packages/react/examples/nano-repl/vite.config.js b/packages/react/examples/nano-repl/vite.config.js index b1b5f91e..627a3196 100644 --- a/packages/react/examples/nano-repl/vite.config.js +++ b/packages/react/examples/nano-repl/vite.config.js @@ -1,7 +1,7 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; // https://vitejs.dev/config/ export default defineConfig({ - plugins: [react()] -}) + plugins: [react()], +}); diff --git a/packages/react/index.html b/packages/react/index.html index 4ec7488d..83d2bd57 100644 --- a/packages/react/index.html +++ b/packages/react/index.html @@ -2,7 +2,7 @@
- +