From 4af0b12b995f722123a818ae8ea626d3ab2b2b3b Mon Sep 17 00:00:00 2001 From: Manuel Cartagena Date: Tue, 6 Oct 2020 21:05:03 -0300 Subject: [PATCH] feat(ableton-link): add ableton link support to orca --- README.md | 1 + desktop/package-lock.json | 27 ++++++++++++++++ desktop/package.json | 1 + desktop/sources/scripts/client.js | 41 +++++++++++++++++++++++++ desktop/sources/scripts/clock.js | 41 ++++++++++++++++++++++--- desktop/sources/scripts/commander.js | 19 ++++++++++-- desktop/sources/scripts/core/io/midi.js | 36 ++++++++++++---------- 7 files changed, 143 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 5bf11a15..4267129f 100644 --- a/README.md +++ b/README.md @@ -149,6 +149,7 @@ All commands have a shorthand equivalent to their first two characters, for exam - `midi:1;2` Set Midi output device to `#1`, and input device to `#2`. - `udp:1234;5678` Set UDP output port to `1234`, and input port to `5678`. - `osc:1234` Set OSC output port to `1234`. +- `link` Enables/Disables Ableton Link ## Base36 Table diff --git a/desktop/package-lock.json b/desktop/package-lock.json index e9197df3..5769b99a 100644 --- a/desktop/package-lock.json +++ b/desktop/package-lock.json @@ -67,6 +67,15 @@ "integrity": "sha512-sz9MF/zk6qVr3pAnM0BSQvYIBK44tS75QC5N+VbWSE4DjCV/pJ+UzCW/F+vVnl7TkOPcuwQureKNtSSwjBTaMg==", "dev": true }, + "abletonlink-addon": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/abletonlink-addon/-/abletonlink-addon-0.2.9.tgz", + "integrity": "sha512-gYsedXCU0edpMBAyGmrMWAKkPwrMgqKT11xKCAixenOWH4X/RiZKC3avVaP94+uQs4J11ujuV7axuIZSMeKp3g==", + "requires": { + "bindings": "^1.5.0", + "node-addon-api": "^2.0.0" + } + }, "asar": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/asar/-/asar-2.1.0.tgz", @@ -101,6 +110,14 @@ "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==", "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==", + "requires": { + "file-uri-to-path": "1.0.0" + } + }, "binpack": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/binpack/-/binpack-0.1.0.tgz", @@ -523,6 +540,11 @@ "pend": "~1.2.0" } }, + "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==" + }, "find-up": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", @@ -914,6 +936,11 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "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==" + }, "node-osc": { "version": "4.1.8", "resolved": "https://registry.npmjs.org/node-osc/-/node-osc-4.1.8.tgz", diff --git a/desktop/package.json b/desktop/package.json index b6df5847..2c6a2c77 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -27,6 +27,7 @@ "electron-packager": "^14.2.1" }, "dependencies": { + "abletonlink-addon": "^0.2.9", "node-osc": "^4.1.8" }, "standard": { diff --git a/desktop/sources/scripts/client.js b/desktop/sources/scripts/client.js index f1e47062..51568cc5 100644 --- a/desktop/sources/scripts/client.js +++ b/desktop/sources/scripts/client.js @@ -11,6 +11,8 @@ /* global Clock */ /* global Theme */ +const AbletonLink = require("abletonlink-addon") + function Client () { this.version = 176 this.library = library @@ -26,6 +28,29 @@ function Client () { this.commander = new Commander(this) this.clock = new Clock(this) + // Ableton Link + this.link = new AbletonLink(); + + this.link.setTempoCallback((newTempo) => { + newTempo = this.link.getTempo(true) + if (this.clock.isLinkEnabled && this.clock.speed.value != newTempo) { + this.clock.setSpeed(newTempo, newTempo, true) + this.clock.setFrame(0) + this.update() + }; + }); + + this.link.setStartStopCallback((startStopState) => { + console.log("startstop: " + startStopState); + if (startStopState && this.clock.isPaused) { + this.clock.play(false, true, true) + } else if (!startStopState && !this.clock.isPaused) { + this.clock.stop(false, true, false) + this.clock.setFrame(0) + this.update() + } + }); + // Settings this.scale = window.devicePixelRatio this.grid = { w: 8, h: 8 } @@ -117,6 +142,7 @@ function Client () { this.acels.set('Midi', 'Next Input Device', 'CmdOrCtrl+,', () => { this.clock.setFrame(0); this.io.midi.selectNextInput() }) this.acels.set('Midi', 'Next Output Device', 'CmdOrCtrl+.', () => { this.clock.setFrame(0); this.io.midi.selectNextOutput() }) this.acels.set('Midi', 'Refresh Devices', 'CmdOrCtrl+Shift+M', () => { this.io.midi.refresh() }) + this.acels.set('Midi', 'Toggle Ableton Link', 'CmdOrCtrl+Shift+L', () => { this.toggleLink() }) this.acels.set('Communication', 'Choose OSC Port', 'alt+O', () => { this.commander.start('osc:') }) this.acels.set('Communication', 'Choose UDP Port', 'alt+U', () => { this.commander.start('udp:') }) @@ -160,6 +186,21 @@ function Client () { this.update() } + this.toggleLink = () => { + if (this.clock.isLinkEnabled) { + this.link.disable() + this.link.disableStartStopSync() + } else { + this.link.enable() + this.link.enableStartStopSync() + this.clock.setSpeed(this.link.getTempo(true), this.link.getTempo(true), true) + if (!this.link.isPlaying()) { + this.clock.stop(false, true) + } + } + this.clock.isLinkEnabled = !this.clock.isLinkEnabled + } + this.update = () => { if (document.hidden === true) { return } this.clear() diff --git a/desktop/sources/scripts/clock.js b/desktop/sources/scripts/clock.js index 95086cfe..4bfdaa62 100644 --- a/desktop/sources/scripts/clock.js +++ b/desktop/sources/scripts/clock.js @@ -9,6 +9,7 @@ function Clock (client) { this.isPaused = true this.timer = null this.isPuppet = false + this.isLinkEnabled = false this.speed = { value: 120, target: 120 } @@ -34,6 +35,15 @@ function Clock (client) { if (value) { this.speed.value = clamp(value, 60, 300) } if (target) { this.speed.target = clamp(target, 60, 300) } if (setTimer === true) { this.setTimer(this.speed.value) } + if (this.isLinkEnabled) { this.setFrame(0) } + } + + this.setSpeedLink = (value) => { + client.link.setTempo(value) + if (!client.link.isPlaying()) { + this.setFrame(0) + client.update() + } } this.modSpeed = function (mod = 0, animate = false) { @@ -56,8 +66,14 @@ function Clock (client) { client.update() } - this.play = function (msg = false, midiStart = false) { - console.log('Clock', 'Play', msg, midiStart) + this.play = function (msg = false, midiStart = false, linkStart = false) { + console.log('Clock', 'Play', msg, midiStart, linkStart) + if (this.isLinkEnabled && this.isPaused && !linkStart) { + this.isPaused = false + this.setSpeed(this.speed.target, this.speed.target, true) + client.link.play() + return + } if (this.isPaused === false && !midiStart) { return } this.isPaused = false if (this.isPuppet === true) { @@ -73,8 +89,17 @@ function Clock (client) { } } - this.stop = function (msg = false) { + this.stop = function (msg = false, linkStop = false) { console.log('Clock', 'Stop') + console.log(this.isLinkEnabled, this.isPaused, linkStop) + if (this.isLinkEnabled && !this.isPaused && !linkStop) { + this.isPaused = true + this.clearTimer() + client.link.stop() + client.io.midi.allNotesOff() + client.io.midi.silence() + return + } if (this.isPaused === true) { return } this.isPaused = true if (this.isPuppet === true) { @@ -159,10 +184,18 @@ function Clock (client) { // UI + this.getUIMessage = function (offset) { + if (this.isLinkEnabled) { + return `link${this.speed.value}${offset}` + } else { + return this.isPuppet === true ? 'midi' : `${this.speed.value}${offset}` + } + } + this.toString = function () { const diff = this.speed.target - this.speed.value const _offset = Math.abs(diff) > 5 ? (diff > 0 ? `+${diff}` : diff) : '' - const _message = this.isPuppet === true ? 'midi' : `${this.speed.value}${_offset}` + const _message = this.getUIMessage(_offset) const _beat = diff === 0 && client.orca.f % 4 === 0 ? '*' : '' return `${_message}${_beat}` } diff --git a/desktop/sources/scripts/commander.js b/desktop/sources/scripts/commander.js index 71ee1dd1..a06bfb3c 100644 --- a/desktop/sources/scripts/commander.js +++ b/desktop/sources/scripts/commander.js @@ -43,9 +43,24 @@ function Commander (client) { play: (p) => { client.clock.play() }, stop: (p) => { client.clock.stop() }, run: (p) => { client.run() }, + link: (p) => { client.toggleLink() }, // Time - apm: (p) => { client.clock.setSpeed(null, p.int) }, - bpm: (p) => { client.clock.setSpeed(p.int, p.int, true) }, + apm: (p) => { + if (client.clock.isLinkEnabled) { + client.clock.setSpeed(null, p.int) + client.clock.setSpeedLink(p.int) + } else { + client.clock.setSpeed(null, p.int) + } + }, + bpm: (p) => { + if (client.clock.isLinkEnabled) { + client.clock.setSpeed(p.int, p.int, true) + client.clock.setSpeedLink(p.int) + } else { + client.clock.setSpeed(p.int, p.int, true) + } + }, frame: (p) => { client.clock.setFrame(p.int) }, rewind: (p) => { client.clock.setFrame(client.orca.f - p.int) }, skip: (p) => { client.clock.setFrame(client.orca.f + p.int) }, diff --git a/desktop/sources/scripts/core/io/midi.js b/desktop/sources/scripts/core/io/midi.js index 7f4875a9..519a9491 100644 --- a/desktop/sources/scripts/core/io/midi.js +++ b/desktop/sources/scripts/core/io/midi.js @@ -122,23 +122,25 @@ function Midi (client) { } this.receive = function (msg) { - switch (msg.data[0]) { - // Clock - case 0xF8: - client.clock.tap() - break - case 0xFA: - console.log('MIDI', 'Start Received') - client.clock.play(false, true) - break - case 0xFB: - console.log('MIDI', 'Continue Received') - client.clock.play() - break - case 0xFC: - console.log('MIDI', 'Stop Received') - client.clock.stop() - break + if (!client.clock.isLinkEnabled) { + switch (msg.data[0]) { + // Clock + case 0xF8: + client.clock.tap() + break + case 0xFA: + console.log('MIDI', 'Start Received') + client.clock.play(false, true) + break + case 0xFB: + console.log('MIDI', 'Continue Received') + client.clock.play() + break + case 0xFC: + console.log('MIDI', 'Stop Received') + client.clock.stop() + break + } } }