Skip to content

Instantly share code, notes, and snippets.

@xavriley
Created June 22, 2018 14:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save xavriley/ad71f97e3f0c6a9173fa52c99810686e to your computer and use it in GitHub Desktop.
Save xavriley/ad71f97e3f0c6a9173fa52c99810686e to your computer and use it in GitHub Desktop.
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