Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ableton-link): add ableton link support to orca #247

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
27 changes: 27 additions & 0 deletions desktop/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"electron-packager": "^14.2.1"
},
"dependencies": {
"abletonlink-addon": "^0.2.9",
"node-osc": "^4.1.8"
},
"standard": {
Expand Down
62 changes: 60 additions & 2 deletions desktop/sources/scripts/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@
/* global Clock */
/* global Theme */

const AbletonLink = require("abletonlink-addon")

function Client () {
this.version = 176
this.version = 177
this.library = library

this.theme = new Theme(this)
Expand All @@ -26,6 +28,36 @@ function Client () {
this.commander = new Commander(this)
this.clock = new Clock(this)

// Ableton Link
this.link = new AbletonLink()
this.numPeers = 0

this.link.setTempoCallback((newTempo) => {
console.log('Ableton Link', 'New Tempo', newTempo)
newTempo = this.link.getTempo(true)
if (this.clock.isLinkEnabled() && this.clock.speed.value != newTempo) {
this.clock.setSpeed(newTempo, newTempo, this.link.isPlaying())
this.update()
};
});

this.link.setStartStopCallback((startStopState) => {
console.log('Ableton Link', startStopState ? 'Start' : 'Stop')
if (startStopState && this.clock.isPaused) {
this.clock.play(false, false, true)
} else if (!startStopState && !this.clock.isPaused) {
this.clock.stop(false)
this.clock.setFrame(0)
this.update()
}
});

this.link.setNumPeersCallback((newNumPeers) => {
console.log('Ableton Link', 'NumPeers: ' + newNumPeers)
this.numPeers = newNumPeers
this.update()
});

// Settings
this.scale = window.devicePixelRatio
this.grid = { w: 8, h: 8 }
Expand Down Expand Up @@ -117,6 +149,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:') })
Expand Down Expand Up @@ -160,6 +193,27 @@ function Client () {
this.update()
}

this.toggleLink = () => {
if (this.clock.isLinkEnabled()) {
console.log('Ableton Link Disabled')
this.link.disable()
this.link.disableStartStopSync()
this.clock.isPuppet = false
this.clock.puppetSource = null
} else if (!this.clock.isLinkEnabled() && !this.clock.isExternalClockActive()){
console.log('Ableton Link Enabled')
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)
}
this.clock.isPuppet = true
this.clock.puppetSource = sourceLink
console.log(this.clock.puppetSource)
}
}

this.update = () => {
if (document.hidden === true) { return }
this.clear()
Expand Down Expand Up @@ -329,7 +383,11 @@ function Client () {
if (this.commander.isActive === true) {
this.write(`${this.commander.query}${this.orca.f % 2 === 0 ? '_' : ''}`, this.grid.w * 0, this.orca.h + 1, this.grid.w * 4)
} else {
this.write(this.orca.f < 25 ? `ver${this.version}` : `${Object.keys(this.source.cache).length} mods`, this.grid.w * 0, this.orca.h + 1, this.grid.w)
if (this.clock.isLinkEnabled()) {
this.write(`${this.numPeers} links`, this.grid.w * 0, this.orca.h + 1, this.grid.w)
} else {
this.write(this.orca.f < 25 ? `ver${this.version}` : `${Object.keys(this.source.cache).length} mods`, this.grid.w * 0, this.orca.h + 1, this.grid.w)
}
this.write(`${this.orca.w}x${this.orca.h}`, this.grid.w * 1, this.orca.h + 1, this.grid.w)
this.write(`${this.grid.w}/${this.grid.h}${this.tile.w !== 10 ? ' ' + (this.tile.w / 10).toFixed(1) : ''}`, this.grid.w * 2, this.orca.h + 1, this.grid.w)
this.write(`${this.clock}`, this.grid.w * 3, this.orca.h + 1, this.grid.w, this.clock.isPuppet ? 3 : this.io.midi.isClock ? 11 : this.clock.isPaused ? 20 : 2)
Expand Down
59 changes: 53 additions & 6 deletions desktop/sources/scripts/clock.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@

/* global Blob */

const sourceClock = 1
const sourceLink = 2

function Clock (client) {
const workerScript = 'onmessage = (e) => { setInterval(() => { postMessage(true) }, e.data)}'
const worker = window.URL.createObjectURL(new Blob([workerScript], { type: 'text/javascript' }))

this.isPaused = true
this.timer = null
this.isPuppet = false
this.puppetSource = null

this.speed = { value: 120, target: 120 }

Expand All @@ -29,11 +33,36 @@ function Clock (client) {
this.setSpeed(this.speed.value + (this.speed.value < this.speed.target ? 1 : -1), null, true)
}

this.isLinkEnabled = function () {
if (this.isPuppet && this.puppetSource == 2) {
return true
} else {
return false
}
}

this.isExternalClockActive = function () {
if (this.isPuppet && this.puppetSource == 1) {
return true
} else {
return false
}
}

this.setSpeed = (value, target = null, setTimer = false) => {
if (this.speed.value === value && this.speed.target === target && this.timer) { return }
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) {
Expand All @@ -56,17 +85,21 @@ 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.isPaused === false && !midiStart) { return }
this.isPaused = false
if (this.isPuppet === true) {
if (this.isExternalClockActive()) {
console.warn('Clock', 'External Midi control')
if (!pulse.frame || midiStart) { // no frames counted while paused (starting from no clock, unlikely) or triggered by MIDI clock START
this.setFrame(0) // make sure frame aligns with pulse count for an accurate beat
pulse.frame = 0
pulse.count = 5 // by MIDI standard next pulse is the beat
}
} else if (this.isLinkEnabled() && !linkStart) {
console.warn('Clock', 'Ableton Link')
this.setSpeed(this.speed.target, this.speed.target, true)
client.link.play()
} else {
if (msg === true) { client.io.midi.sendClockStart() }
this.setSpeed(this.speed.target, this.speed.target, true)
Expand All @@ -77,8 +110,12 @@ function Clock (client) {
console.log('Clock', 'Stop')
if (this.isPaused === true) { return }
this.isPaused = true
if (this.isPuppet === true) {
if (this.isExternalClockActive()) {
console.warn('Clock', 'External Midi control')
} else if (this.isLinkEnabled()) {
console.warn('Clock', 'Ableton Link')
this.clearTimer()
client.link.stop()
} else {
if (msg === true || client.io.midi.isClock) { client.io.midi.sendClockStop() }
this.clearTimer()
Expand All @@ -99,9 +136,10 @@ function Clock (client) {
this.tap = function () {
pulse.count = (pulse.count + 1) % 6
pulse.last = performance.now()
if (!this.isPuppet) {
if (!this.isPuppet && !this.isLinkEnabled()) {
console.log('Clock', 'Puppeteering starts..')
this.isPuppet = true
this.puppetSource = sourceClock
this.clearTimer()
pulse.timer = setInterval(() => {
if (performance.now() - pulse.last < 2000) { return }
Expand All @@ -124,6 +162,7 @@ function Clock (client) {
console.log('Clock', 'Puppeteering stops..')
clearInterval(pulse.timer)
this.isPuppet = false
this.puppetSource = null
pulse.frame = 0
pulse.last = null
if (!this.isPaused) {
Expand Down Expand Up @@ -159,10 +198,18 @@ function Clock (client) {

// UI

this.getUIMessage = function (offset) {
if (this.isLinkEnabled()) {
return `link${this.speed.value}`
} else {
return this.isExternalClockActive() ? '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}`
}
Expand Down
23 changes: 21 additions & 2 deletions desktop/sources/scripts/commander.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,28 @@ 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()) {
if (client.link.isPlaying()) {
client.clock.setSpeed(p.int, p.int, true)
} else {
client.clock.setSpeed(p.int, p.int, false)
}
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) },
Expand Down
36 changes: 19 additions & 17 deletions desktop/sources/scripts/core/io/midi.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}

Expand Down