aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore6
-rw-r--r--.gitmodules3
-rw-r--r--README.md56
-rw-r--r--abletonlink-1.0.0-1.rockspec58
-rw-r--r--abletonlink.c172
m---------link0
-rw-r--r--spec/abletonlink_spec.lua81
7 files changed, 376 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..59e8233
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+*.o
+*.so
+*.lib
+*.dll
+
+*.rock
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..a29771c
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "link"]
+ path = link
+ url = https://github.com/Ableton/link.git
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..348018a
--- /dev/null
+++ b/README.md
@@ -0,0 +1,56 @@
+abletonlink
+===========
+
+Lightweight Lua wrapper of the Ableton Link C API ([abl_link][abl_link]).
+
+API
+---
+
+The module follows the C API very closely.
+See the [`abl_link.h`][abl_link.h] for comments for the various methods.
+
+The module table has three members:
+
+- `abletonlink._VERSION`: a string like `"1.0.0"`
+- `abletonlink.create(number bpm)`: creates an `abl_link` instance
+- `abletonlink.create_session_state()`: creates an `abl_link_session_state` instance
+
+`abl_link` instances have the following methods:
+
+- `link:is_enabled() -> bool`
+- `link:enable(bool?)` (defaults to true)
+- `link:is_start_stop_sync_enabled() -> bool`
+- `link:enable_start_stop_sync(bool?)` (defaults to true)
+- `link:num_peers() -> int`
+- `link:clock_micros() -> int`
+- `link:capture_audio_session_state(session_state output)`
+- `link:commit_audio_session_state(session_state input)`
+- `link:capture_app_session_state(session_state output)`
+- `link:commit_app_session_state(session_state input)`
+- `link:destroy()`:
+ Destroys this instance.
+ Calling any other method on a destroyed instance causes an error.
+ This is automatically called in __gc.
+- `link:set_*_callback` are not currently implemented.
+
+`abl_link_session_state` instances have the following methods:
+
+- `session_state:tempo() -> number`
+- `session_state:set_tempo(number bpm, int at_time)`
+- `session_state:beat_at_time(int time, number quantum) -> number`
+- `session_state:phase_at_time(int time, number quantum) -> number`
+- `session_state:time_at_beat(number beat, number quantum) -> int`
+- `session_state:request_beat_at_time(number beat, int time, number quantum)`
+- `session_state:force_beat_at_time(number beat, int time, number quantum)`
+- `session_state:set_is_playing(bool, int at_time)`
+- `session_state:is_playing() -> bool`
+- `session_state:time_for_is_playing() -> int`
+- `session_state:request_beat_at_start_playing_time(number beat, number quantum)`
+- `session_state:set_is_playing_and_request_beat_at_time(bool, int time, number beat, number quantum)`
+- `session_state:destroy()`:
+ Destroys this instance.
+ Calling any other method on a destroyed instance causes an error.
+ This is automatically called in __gc.
+
+[abl_link]: https://github.com/Ableton/link/tree/master/extensions/abl_link
+[abl_link.h]: https://github.com/Ableton/link/blob/master/extensions/abl_link/include/abl_link.h
diff --git a/abletonlink-1.0.0-1.rockspec b/abletonlink-1.0.0-1.rockspec
new file mode 100644
index 0000000..ee883bb
--- /dev/null
+++ b/abletonlink-1.0.0-1.rockspec
@@ -0,0 +1,58 @@
+rockspec_format = "3.0"
+package = "abletonlink"
+version = "1.0.0-1"
+source = {
+ url = 'git+https://git.s-ol.nu/lua-abletonlink.git',
+ tag = 'v1.0.0',
+}
+description = {
+ summary = "Ableton Link bindings for Lua",
+ detailed = [[
+Lightweight wrapper of the Ableton Link C API (abl_link).
+
+https://github.com/Ableton/link/tree/master/extensions/abl_link
+ ]],
+ homepage = "https://git.s-ol.nu/lua-abletonlink/-/",
+ license = "GPL-2.0-or-later",
+}
+
+dependencies = {
+ "lua >= 5.1",
+}
+build = {
+ type = 'builtin',
+ modules = {
+ -- C module
+ abletonlink = {
+ sources = {
+ 'abletonlink.c',
+ 'link/extensions/abl_link/src/abl_link.cpp',
+ },
+ incdirs = {'link/include', 'link/extensions/abl_link/include'},
+ libraries = {'stdc++'},
+ },
+ },
+ platforms = {
+ linux = {
+ modules = {
+ abletonlink = {
+ defines = {'LINK_PLATFORM_LINUX'},
+ },
+ },
+ },
+ windows = {
+ modules = {
+ abletonlink = {
+ defines = {'LINK_PLATFORM_WINDOWS'},
+ },
+ },
+ },
+ macosx = {
+ modules = {
+ abletonlink = {
+ defines = {'LINK_PLATFORM_MACOSX'},
+ },
+ },
+ },
+ },
+}
diff --git a/abletonlink.c b/abletonlink.c
new file mode 100644
index 0000000..c6649b5
--- /dev/null
+++ b/abletonlink.c
@@ -0,0 +1,172 @@
+#include <lua.h>
+#include <lauxlib.h>
+#include "link/extensions/abl_link/include/abl_link.h"
+
+#if LUA_VERSION_NUM > 501
+#define luax_len(L, i) (int) lua_rawlen(L, i)
+#define luax_register(L, f) luaL_setfuncs(L, f, 0)
+#else
+#define luax_len(L, i) (int) lua_objlen(L, i)
+#define luax_register(L, f) luaL_register(L, NULL, f)
+#define LUA_RIDX_MAINTHREAD 1
+#endif
+
+static int l_create_link(lua_State* L) {
+ double bpm = luaL_checknumber(L, 1);
+
+ abl_link* link = (abl_link*)lua_newuserdata(L, sizeof(abl_link));
+
+ *link = abl_link_create(bpm);
+
+ luaL_getmetatable(L, "abletonlink.link");
+ lua_setmetatable(L, -2);
+
+ return 1;
+}
+
+static abl_link checklink(lua_State *L) {
+ abl_link* link = (abl_link*)luaL_checkudata(L, 1, "abletonlink.link");
+ luaL_argcheck(L, link != NULL, 1, "`link' expected");
+ luaL_argcheck(L, link->impl != NULL, 1, "`link' is already destroyed");
+ return *link;
+}
+
+static int l_destroy_link(lua_State* L) {
+ abl_link* link = (abl_link*)luaL_checkudata(L, 1, "abletonlink.link");
+ luaL_argcheck(L, link != NULL, 1, "`link' expected");
+ if (link->impl) {
+ abl_link_destroy(*link);
+ link->impl = NULL;
+ }
+ return 0;
+}
+
+static int l_is_enabled(lua_State* L) {
+ abl_link link = checklink(L);
+ bool enabled = abl_link_is_enabled(link);
+ lua_pushboolean(L, enabled);
+ return 1;
+}
+
+static int l_enable(lua_State* L) {
+ abl_link link = checklink(L);
+ bool enable = lua_isnoneornil(L, 2) ? true : lua_toboolean(L, 2);
+ abl_link_enable(link, enable);
+ return 0;
+}
+
+static int l_is_start_stop_sync_enabled(lua_State* L) {
+ abl_link link = checklink(L);
+ bool enabled = abl_link_is_start_stop_sync_enabled(link);
+ lua_pushboolean(L, enabled);
+ return 1;
+}
+
+static int l_enable_start_stop_sync(lua_State* L) {
+ abl_link link = checklink(L);
+ bool enable = lua_isnoneornil(L, 2) ? true : lua_toboolean(L, 2);
+ abl_link_enable_start_stop_sync(link, enable);
+ return 0;
+}
+
+static int l_num_peers(lua_State* L) {
+ abl_link link = checklink(L);
+ uint64_t peers = abl_link_num_peers(link);
+ lua_pushinteger(L, peers);
+ return 1;
+}
+
+static int l_clock_micros(lua_State* L) {
+ abl_link link = checklink(L);
+ int64_t micros = abl_link_clock_micros(link);
+ lua_pushinteger(L, micros);
+ return 1;
+}
+
+static const luaL_Reg abl_link_r[] = {
+ { "destroy", l_destroy_link },
+ { "is_enabled", l_is_enabled},
+ { "enable", l_enable },
+ { "is_start_stop_sync_enabled", l_is_start_stop_sync_enabled },
+ { "enable_start_stop_sync", l_enable_start_stop_sync},
+ { "num_peers", l_num_peers },
+ { "clock_micros", l_clock_micros },
+ /*
+ { "capture_audio_session_state", l_capture_audio_session_state },
+ { "commit_audio_session_state", l_commit_audio_session_state },
+ { "capture_app_session_state", l_capture_app_session_state },
+ { "commit_app_session_state", l_commit_app_session_state },
+ */
+ { "__gc", l_destroy_link },
+ { NULL, NULL },
+};
+
+static abl_link_session_state* checksessionstate(lua_State *L) {
+ void *ud = luaL_checkudata(L, 1, "abletonlink.session_state");
+ luaL_argcheck(L, ud != NULL, 1, "`session_state' expected");
+ return (abl_link_session_state*)ud;
+}
+
+static int l_destroy_session_state(lua_State* L) {
+ abl_link_session_state* state = checksessionstate(L);
+ if (state->impl) {
+ abl_link_destroy_session_state(*state);
+ state->impl = NULL;
+ }
+ return 0;
+}
+
+static const luaL_Reg abl_link_session_state_r[] = {
+ { "destroy", l_destroy_session_state },
+ /*
+ { "tempo", l_tempo },
+ { "set_tempo", l_set_tempo },
+ { "beat_at_time", l_beat_at_time },
+ { "phase_at_time", l_phase_at_time },
+ { "time_at_beat", l_time_at_beat },
+ { "request_beat_at_time", l_request_beat_at_time },
+ { "force_beat_at_time", l_force_beat_at_time },
+ { "is_playing", l_is_playing },
+ { "set_is_playing", l_set_is_playing },
+ { "time_for_is_playing", l_time_for_is_playing },
+ { "request_beat_at_start_playing_time", l_request_beat_at_start_playing_time },
+ { "set_is_playing_and_request_beat_at_time", l_set_is_playing_and_request_beat_at_time },
+ */
+ { "__gc", l_destroy_session_state },
+ { NULL, NULL },
+};
+
+static const luaL_Reg abletonlink[] = {
+ { "create", l_create_link },
+ { NULL, NULL },
+};
+
+int luaopen_abletonlink(lua_State* L) {
+ luaL_newmetatable(L, "abletonlink.link");
+ lua_getmetatable(L, -1);
+
+ // m.__index = m
+ lua_pushvalue(L, -1);
+ lua_setfield(L, -1, "__index");
+
+ // register
+ luax_register(L, abl_link_r);
+ lua_pop(L, 1);
+
+ luaL_newmetatable(L, "abletonlink.session_state");
+ lua_getmetatable(L, -1);
+
+ // m.__index = m
+ lua_pushvalue(L, -1);
+ lua_setfield(L, -1, "__index");
+
+ // register
+ luax_register(L, abl_link_session_state_r);
+ lua_pop(L, 1);
+
+ lua_newtable(L);
+ lua_pushstring(L, "v1.0.0");
+ lua_setfield(L, -2, "_VERSION");
+ luax_register(L, abletonlink);
+ return 1;
+}
diff --git a/link b/link
new file mode 160000
+Subproject a33d2c34997dc8679ba9092652dd52bffc0c7b0
diff --git a/spec/abletonlink_spec.lua b/spec/abletonlink_spec.lua
new file mode 100644
index 0000000..a2e4398
--- /dev/null
+++ b/spec/abletonlink_spec.lua
@@ -0,0 +1,81 @@
+local assert = require 'luassert'
+local say = require("say")
+local function greater_than(state, arguments)
+ local min, val = arguments[1], arguments[2]
+ return min < val
+end
+assert:register('assertion', 'greater_than', greater_than, 'assertion.greater_than.positive', 'assertion.greater_than.negative')
+say:set_namespace("en")
+say:set("assertion.greater_than.positive", "Expected %s < %s")
+say:set("assertion.greater_than.negative", "Expected %s >= %s")
+
+describe("abletonlink library", function()
+ local link = require 'abletonlink'
+
+ it("exports a _VERSION", function()
+ -- asset.is.equal('1.0.0', link._VERSION)
+ end)
+
+ it("can create and destroy link objects", function()
+ local l = link.create(120)
+ l:destroy()
+
+ assert.has.error(function()
+ l:is_enabled()
+ end)
+ end)
+
+ it("can be enabled and disabled", function()
+ local l = link.create(120)
+ assert.is_false(l:is_enabled())
+ l:enable()
+ assert.is_true(l:is_enabled())
+
+ l:enable(false)
+ assert.is_false(l:is_enabled())
+ l:enable(true)
+ assert.is_true(l:is_enabled())
+ end)
+
+ it("start/stop sync can be enabled and disabled", function()
+ local l = link.create(120)
+ assert.is_false(l:is_start_stop_sync_enabled())
+ l:enable_start_stop_sync()
+ assert.is_true(l:is_start_stop_sync_enabled())
+
+ l:enable_start_stop_sync(false)
+ assert.is_false(l:is_start_stop_sync_enabled())
+ l:enable_start_stop_sync(true)
+ assert.is_true(l:is_start_stop_sync_enabled())
+ end)
+
+ test("num_peers()", function()
+ local a, b = link.create(120), link.create(120)
+ assert.equal(0, a:num_peers())
+ assert.equal(0, b:num_peers())
+
+ a:enable()
+ b:enable()
+
+ -- stall a bit for connections to succeed
+ for i=1,10000 do
+ i = math.random()
+ end
+
+ assert.greater_than(0, a:num_peers())
+ assert.greater_than(0, b:num_peers())
+ end)
+
+ test("clock_micros()", function()
+ local l = link.create(120)
+
+ local last = 0
+
+ for i=1,10 do
+ local next = l:clock_micros()
+ assert.is.number(next)
+ assert.is.greater_than(last, next)
+ last = next
+ end
+ end)
+end)