From eeaead66b51072f2d27ba5594edb91ae075d7e6c Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 26 Jan 2023 21:46:57 +0100 Subject: [PATCH] ableton link mvp --- package-lock.json | 137 ++++++++++++++++++++++++++++++++++ package.json | 4 + packages/link/link-server.mjs | 62 +++++++++++++++ 3 files changed, 203 insertions(+) create mode 100644 packages/link/link-server.mjs diff --git a/package-lock.json b/package-lock.json index 78671f8f2..8e52d37af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,8 +11,12 @@ "workspaces": [ "packages/*" ], + "dependencies": { + "node-web-audio-api": "^0.5.0" + }, "devDependencies": { "@vitest/ui": "^0.25.7", + "abletonlink": "^0.1.3", "c8": "^7.12.0", "canvas": "^2.11.0", "dependency-tree": "^9.0.0", @@ -1822,6 +1826,34 @@ "node": ">= 6" } }, + "node_modules/@napi-rs/cli": { + "version": "2.14.4", + "resolved": "https://registry.npmjs.org/@napi-rs/cli/-/cli-2.14.4.tgz", + "integrity": "sha512-JvnJWFGhqjDV+D5MD9HCPH7Bc8uxwyPbpA8jsS5mtAp9+3IhVxpqhQc+eqmJrDAPO9z3a8qAdNnBf9+ZI9bIQg==", + "bin": { + "napi": "scripts/index.js" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/triples": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@napi-rs/triples/-/triples-1.1.0.tgz", + "integrity": "sha512-XQr74QaLeMiqhStEhLn1im9EOMnkypp7MZOwQhGzqp2Weu5eQJbpPxWxixxlYRKWPOmJjsk6qYfYH9kq43yc2w==" + }, + "node_modules/@node-rs/helper": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@node-rs/helper/-/helper-1.3.3.tgz", + "integrity": "sha512-p4OdfQObGN9YFy5WZaGwlPYICQSe7xZYyXB0sxREmvj1HzGKp5bPg2PlfgfMZEfnjIA882B9ZrnagYzZihIwjA==", + "dependencies": { + "@napi-rs/triples": "^1.1.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "license": "MIT", @@ -2611,6 +2643,20 @@ "dev": true, "license": "ISC" }, + "node_modules/abletonlink": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/abletonlink/-/abletonlink-0.1.3.tgz", + "integrity": "sha512-rPMS0X6+wcW607Lpr3XeqfMJ26oNDc/Yx6+5oEtkS9CYr2rKRh6dfiaHpDxhoEqKZtBrZY0JWOlkZhD49tesjQ==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "bindings": "^1.5.0", + "node-addon-api": "^2.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/acorn": { "version": "7.4.1", "license": "MIT", @@ -3028,6 +3074,15 @@ "node": ">=8" } }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, "node_modules/bl": { "version": "4.1.0", "dev": true, @@ -5399,6 +5454,12 @@ "node": ">=10" } }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true + }, "node_modules/filename-reserved-regex": { "version": "2.0.0", "dev": true, @@ -8509,6 +8570,12 @@ "dev": true, "license": "MIT" }, + "node_modules/node-addon-api": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.2.tgz", + "integrity": "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==", + "dev": true + }, "node_modules/node-domexception": { "version": "1.0.0", "dev": true, @@ -8699,6 +8766,18 @@ "node": ">=6.0" } }, + "node_modules/node-web-audio-api": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/node-web-audio-api/-/node-web-audio-api-0.5.0.tgz", + "integrity": "sha512-HMFQHti2RnhgAzIrK6PSpDqgwKHMdeWsVILBwEQcCWzol9nGDK+QQQCJPWpMTfvTuGsfEletzRqz+p6fhcjDuA==", + "dependencies": { + "@napi-rs/cli": "^2.10.0", + "@node-rs/helper": "^1.3.3" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/nopt": { "version": "4.0.3", "dev": true, @@ -14477,6 +14556,24 @@ } } }, + "@napi-rs/cli": { + "version": "2.14.4", + "resolved": "https://registry.npmjs.org/@napi-rs/cli/-/cli-2.14.4.tgz", + "integrity": "sha512-JvnJWFGhqjDV+D5MD9HCPH7Bc8uxwyPbpA8jsS5mtAp9+3IhVxpqhQc+eqmJrDAPO9z3a8qAdNnBf9+ZI9bIQg==" + }, + "@napi-rs/triples": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@napi-rs/triples/-/triples-1.1.0.tgz", + "integrity": "sha512-XQr74QaLeMiqhStEhLn1im9EOMnkypp7MZOwQhGzqp2Weu5eQJbpPxWxixxlYRKWPOmJjsk6qYfYH9kq43yc2w==" + }, + "@node-rs/helper": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@node-rs/helper/-/helper-1.3.3.tgz", + "integrity": "sha512-p4OdfQObGN9YFy5WZaGwlPYICQSe7xZYyXB0sxREmvj1HzGKp5bPg2PlfgfMZEfnjIA882B9ZrnagYzZihIwjA==", + "requires": { + "@napi-rs/triples": "^1.1.0" + } + }, "@nodelib/fs.scandir": { "version": "2.1.5", "requires": { @@ -15236,6 +15333,16 @@ "version": "1.1.1", "dev": true }, + "abletonlink": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/abletonlink/-/abletonlink-0.1.3.tgz", + "integrity": "sha512-rPMS0X6+wcW607Lpr3XeqfMJ26oNDc/Yx6+5oEtkS9CYr2rKRh6dfiaHpDxhoEqKZtBrZY0JWOlkZhD49tesjQ==", + "dev": true, + "requires": { + "bindings": "^1.5.0", + "node-addon-api": "^2.0.0" + } + }, "acorn": { "version": "7.4.1" }, @@ -15489,6 +15596,15 @@ "version": "2.2.0", "dev": true }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "requires": { + "file-uri-to-path": "1.0.0" + } + }, "bl": { "version": "4.1.0", "dev": true, @@ -17074,6 +17190,12 @@ } } }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true + }, "filename-reserved-regex": { "version": "2.0.0", "dev": true @@ -19107,6 +19229,12 @@ "version": "2.6.2", "dev": true }, + "node-addon-api": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.2.tgz", + "integrity": "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==", + "dev": true + }, "node-domexception": { "version": "1.0.0", "dev": true @@ -19236,6 +19364,15 @@ "@babel/parser": "^7.0.0" } }, + "node-web-audio-api": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/node-web-audio-api/-/node-web-audio-api-0.5.0.tgz", + "integrity": "sha512-HMFQHti2RnhgAzIrK6PSpDqgwKHMdeWsVILBwEQcCWzol9nGDK+QQQCJPWpMTfvTuGsfEletzRqz+p6fhcjDuA==", + "requires": { + "@napi-rs/cli": "^2.10.0", + "@node-rs/helper": "^1.3.3" + } + }, "nopt": { "version": "4.0.3", "dev": true, diff --git a/package.json b/package.json index 6d60cfb72..530fb580c 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "homepage": "https://strudel.tidalcycles.org", "devDependencies": { "@vitest/ui": "^0.25.7", + "abletonlink": "^0.1.3", "c8": "^7.12.0", "canvas": "^2.11.0", "dependency-tree": "^9.0.0", @@ -61,5 +62,8 @@ "prettier": "^2.8.1", "rollup-plugin-visualizer": "^5.8.1", "vitest": "^0.25.7" + }, + "dependencies": { + "node-web-audio-api": "^0.5.0" } } diff --git a/packages/link/link-server.mjs b/packages/link/link-server.mjs new file mode 100644 index 000000000..02753d9a0 --- /dev/null +++ b/packages/link/link-server.mjs @@ -0,0 +1,62 @@ +//// get pattern + +import { evaluate } from '@strudel.cycles/transpiler'; +import { evalScope, controls, getFrequency } from '@strudel.cycles/core'; + +await evalScope( + controls, + import('@strudel.cycles/core'), + import('@strudel.cycles/tonal'), + import('@strudel.cycles/mini'), + // import('@strudel.cycles/xen'), + // import('@strudel.cycles/osc'), +); + +const { pattern } = await evaluate(`note("".voicings('lefthand')).gain(.25).s('sawtooth')`); + +//// web audio + +import { AudioContext, OscillatorNode, GainNode } from 'node-web-audio-api'; + +const ac = new AudioContext(); + +export function playHaps(ac, haps, beat, cps = 1) { + const ct = ac.currentTime; + haps + .filter((hap) => hap.hasOnset()) + .forEach((hap) => { + const deadline = (hap.whole.begin - beat) / cps; + const t = ct + deadline; + const freq = getFrequency(hap) ?? 220; + const { gain = 1, s = 'triangle' } = hap.value; + + const env = new GainNode(ac); + env.connect(ac.destination); + env.gain.value = 0; + env.gain.setValueAtTime(0, t); + env.gain.linearRampToValueAtTime(gain, t + 0.02); + env.gain.exponentialRampToValueAtTime(0.0001, t + hap.duration); + + const osc = new OscillatorNode(ac); + osc.frequency.value = freq; + osc.type = s; + osc.connect(env); + osc.start(t); + osc.stop(t + hap.duration); + }); +} + +//// link + +import abletonlink from 'abletonlink'; +const link = new abletonlink(); + +let lastBeat; +link.startUpdate(60, (beat, phase, bpm) => { + if (lastBeat !== undefined) { + const cps = bpm / 60; + const haps = pattern.slow(2).queryArc(lastBeat, beat); + playHaps(ac, haps, lastBeat, cps); + } + lastBeat = beat; +});