From 428b68cfd0b767cdfc716ffb68f8746e3ae192e0 Mon Sep 17 00:00:00 2001 From: s-ol Date: Tue, 13 Dec 2022 13:31:29 +0100 Subject: implement simple get/setters --- .gitignore | 6 ++ .gitmodules | 3 + README.md | 56 ++++++++++++++ abletonlink-1.0.0-1.rockspec | 58 +++++++++++++++ abletonlink.c | 172 +++++++++++++++++++++++++++++++++++++++++++ link | 1 + spec/abletonlink_spec.lua | 81 ++++++++++++++++++++ 7 files changed, 377 insertions(+) create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 README.md create mode 100644 abletonlink-1.0.0-1.rockspec create mode 100644 abletonlink.c create mode 160000 link create mode 100644 spec/abletonlink_spec.lua 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 +#include +#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 index 0000000..a33d2c3 --- /dev/null +++ b/link @@ -0,0 +1 @@ +Subproject commit a33d2c34997dc8679ba9092652dd52bffc0c7b04 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) -- cgit v1.2.3