Skip to content

Instantly share code, notes, and snippets.

@xavriley
Created Jun 22, 2018
Embed
What would you like to do?
Working with Carabiner (Ableton Link) from Ruby

Working with Carabiner (Ableton Link) from Ruby

This script currently requires Ruby >= 2.4 and Mac OS 10.12 or higher

Download Carabiner from here https://github.com/brunchboy/carabiner (releases page has builds for OSX and Windows)

Run it in a terminal somewhere.

Download the Link repo and build the examples:

git clone git@github.com:Ableton/link.git
cd link
mkdir build && cd build
export CMAKE_PREFIX_PATH="/usr/local/opt/qt/" # I had to do this for Homebrew's Qt to be picked up
cmake -DLINK_BUILD_QT_EXAMPLES=ON ..
cmake --build .
bin/QLinkHut

Enable start/stop sync on the gui example

Then run

ruby carabiner.rb
require 'socket'
class Carabiner
DEFAULT_QUANTUM = 4
def initialize
# assumes Carabiner has already been started
@socket = TCPSocket.open("localhost", 17000)
end
def status
command("status")
end
# convenience function
def time_now_in_micros
# this may vary across platforms and Ruby versions
# For parity with Link we need Process::CLOCK_UPTIME_RAW for
# > mac os 10.12 and > Ruby 2.4
# else
# Process::CLOCK_MONOTONIC
Process.clock_gettime(Process::CLOCK_UPTIME_RAW, :microsecond)
end
def set_bpm(bpm)
# todo clamp to within 20.0 to 999.0
command("bpm", bpm)
end
def beat_at_time(micro_time, quantum = DEFAULT_QUANTUM)
command("beat-at-time", micro_time, quantum)
end
def phase_at_time(micro_time, quantum = DEFAULT_QUANTUM)
command("phase-at-time", micro_time, quantum)
end
def time_at_beat(beat, quantum = DEFAULT_QUANTUM)
command("time-at-beat", beat, quantum)
end
def force_beat_at_time!(beat, micro_time, quantum = DEFAULT_QUANTUM)
# WARNING: This command should only be used when synchronizing a Link
# session to an external timing source which cannot participate in the
# consensus-based Link timeline, and should be done only when the session
# has drifted beyond some reasonable threshold, so that external jitter does
# not lead to excessive adjustments.
# If you are building an application that can perform quantized starts, and
# thereby participate in a Link session more graciously, without requiring
# the other participants to shift the timeline, you should use the following
# command instead:
command("force-beat-at-time", beat, micro_time, quantum)
end
def request_beat_at_time(beat, micro_time, quantum = DEFAULT_QUANTUM)
# Ask Link to try to gracefully adjust its timeline so that the specified
# beat will occur at the specified time. If there are no other peers in the
# Link session, this will behave the same as force-beat-at-time, above.
# However, if there are any peers, it will avoid the kinds of audible
# discontinuities described above, by adjusting the local timeline so that
# the specified beat will instead fall at the next point in time after the
# requested time which has the same phase as the specified beat.
# Carabiner responds with a status message which reports the new :start
# timestamp of the timeline.
command("request-beat-at-time", beat, micro_time, quantum)
end
def enable_start_stop_sync
command("enable-start-stop-sync")
end
def disable_start_stop_sync
command("disable-start-stop-sync")
end
def start_playing_at(micro_time)
command("start-playing", micro_time)
end
def stop_playing_at(micro_time)
command("stop-playing", micro_time)
end
private
def command(*args)
# example response returned by carabiner
# "status { :peers 1 :bpm 120.000000 :start 17526103328 :beat 7521.742530 }"
puts "DEBUG *#{args.join(" ")}*"
@socket.sendmsg args.join(" ")
res = @socket.recvmsg[0]
if res[/unsupported|bad/]
$stderr.puts "Received bad response for '#{args.join(' ')}': #{res}"
return
end
edn_data = res.
# strip "status { ... }"
match(/\w+ {([^\}]+)}/)[1].strip.
# split on ":<key> <value>"
split(/:([^\s]+)\s/).
# reject blank "" at start
slice(1..-1)
# [key, val, key, val] has a to_hash method
# vals are currently strings - use eval to get the proper Ruby types
edn_data.each_slice(2).collect {|k,v| [k.to_sym, eval(v)] }.to_h
end
end
# inline tests ftw
ca = Carabiner.new
ca.set_bpm(80)
5.times do
puts ca.status
end
ca.set_bpm(120)
5.times do
puts ca.status
end
puts "Time now in microseconds: #{ca.time_now_in_micros}"
puts "Time of beat 500us from now: #{ca.beat_at_time(ca.time_now_in_micros + 500)}"
puts "Time of phase 500us from now: #{ca.phase_at_time(ca.time_now_in_micros + 500)}"
puts "Time at beat 100: #{ca.time_at_beat(100)}"
puts "Force resetting beat 4 to be 500us from now"
puts ""
ca.force_beat_at_time!(4, ca.time_now_in_micros + 500)
puts "Time of beat now: #{ca.beat_at_time(ca.time_now_in_micros + 500)}"
puts ""
puts "Gracefully resetting beat 1 to be 500us from now"
puts ""
ca.request_beat_at_time(4, ca.time_now_in_micros + 500)
puts "Time of beat now: #{ca.beat_at_time(ca.time_now_in_micros + 500)}"
puts "Enable start/stop sync #{ca.enable_start_stop_sync}"
puts
sleep 1
puts "Start playing #{ca.start_playing_at(ca.time_now_in_micros)}"
puts
sleep 4
puts "Stop playing #{ca.stop_playing_at(ca.time_now_in_micros)}"
puts
puts "Disable start/stop sync #{ca.disable_start_stop_sync}"
puts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment