/**
* pustule
* Copyright 2014-2020 Thomas Jost <schnouki@schnouki.net>
*
* This file is part of pustule.
*
* pustule is free software: you can redistribute it and/or modify it under the
* terms of the GNU General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any later
* version.
*
* pustule is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* pustule. If not, see <http://www.gnu.org/licenses/>.
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
#include <pulse/pulseaudio.h>
#include "log.h"
#include "pustule.h"
/* Debug helper */
void _dump_stack (lua_State *L) {
int top = lua_gettop(L);
printf("LUA STACK: ");
for (int i = 1; i <= top; i++) {
printf("%d:", i);
int t = lua_type(L, i);
switch (t) {
case LUA_TSTRING:
printf("'%s'", lua_tostring(L, i));
break;
case LUA_TBOOLEAN:
printf(lua_toboolean(L, i) ? "true" : "false");
break;
case LUA_TNUMBER:
if (lua_isinteger(L, i))
printf("%lld", lua_tointeger(L, i));
else
printf("%g", lua_tonumber(L, i));
break;
default:
printf("%s", lua_typename(L, t));
break;
}
printf(" ");
}
printf("\n");
}
/* Helper for protected calls */
bool check_pcall_error(lua_State* L, int res) {
if (res == LUA_OK)
return false;
const char* msg = lua_tostring(L, -1);
if (res == LUA_ERRRUN) {
log_warn("Lua runtime error: %s", msg);
return true;
}
char* type = "unknown error";
if (res == LUA_ERRMEM) type = "memory error";
else if (res == LUA_ERRERR) type = "message handler error";
else if (res == LUA_ERRGCMM) type = "__gc error";
log_error("Lua %s: %s", type, msg);
return true;
}
/* Signal listeners */
lua_State* g_L;
array_t input_added_listeners;
array_t input_removed_listeners;
array_t device_added_listeners;
array_t device_removed_listeners;
/* Query helpers */
static void _query_number_cb(lua_Number number, void* userdata) {
intptr_t cb_ref = (intptr_t) userdata;
lua_rawgeti(g_L, LUA_REGISTRYINDEX, cb_ref);
lua_pushnumber(g_L, number);
lua_call(g_L, 1, 0);
luaL_unref(g_L, LUA_REGISTRYINDEX, cb_ref);
}
static void _query_device_name_cb(pustule_server_info_t* info, void* userdata) {
intptr_t cb_ref = (intptr_t) userdata;
lua_rawgeti(g_L, LUA_REGISTRYINDEX, cb_ref);
lua_pushstring(g_L, info->default_sink_name);
lua_call(g_L, 1, 0);
luaL_unref(g_L, LUA_REGISTRYINDEX, cb_ref);
}
/* Lua query functions */
/**
* Query the volume of an input.
* @function get_input_volume
* @param idx input index
* @param callback function called with 1 parameter (volume as a number) when the volume is available
*/
int pustule_get_input_volume(lua_State* L) {
uint32_t idx = luaL_checkinteger(L, 1);
intptr_t cb_ref = luaL_ref(L, LUA_REGISTRYINDEX);
input_get_volume(idx, _query_number_cb, (void*) cb_ref);
return 0;
}
/**
* Set the volume of an input.
* @function set_input_volume
* @param idx input index
* @param vol new volume value (0-1)
*/
int pustule_set_input_volume(lua_State* L) {
uint32_t idx = luaL_checkinteger(L, 1);
double volume = luaL_checknumber(L, 2);
pa_cvolume cvol;
pa_cvolume_init(&cvol);
pa_volume_t new_vol = (pa_volume_t) (volume * PA_VOLUME_NORM);
pa_cvolume_set(&cvol, 1, new_vol);
pa_operation* op = pa_context_set_sink_input_volume(pactx, idx, &cvol, NULL, NULL);
pa_operation_unref(op);
return 0;
}
/**
* Query the volume of a device.
* @function get_input_volume
* @param idx input index
* @param callback function called with 1 parameter (volume as a number) when the volume is available
*/
int pustule_get_device_volume(lua_State* L) {
uint32_t idx = luaL_checkinteger(L, 1);
intptr_t cb_ref = luaL_ref(L, LUA_REGISTRYINDEX);
device_get_volume(idx, _query_number_cb, (void*) cb_ref);
return 0;
}
/**
* Set the volume for a device.
* @function set_device_volume
* @param idx device index
* @param vol new volume value (0-1)
*/
int pustule_set_device_volume(lua_State* L) {
uint32_t idx = luaL_checkinteger(L, 1);
double volume = luaL_checknumber(L, 2);
pa_cvolume cvol;
pa_cvolume_init(&cvol);
pa_volume_t new_vol = (pa_volume_t) (volume * PA_VOLUME_NORM);
pa_cvolume_set(&cvol, 1, new_vol);
pa_operation* op = pa_context_set_sink_volume_by_index(pactx, idx, &cvol, NULL, NULL);
pa_operation_unref(op);
return 0;
}
/**
* Get the default device (by name).
* @function get_default_device
* @param callback function called with the default device name
*/
int pustule_get_default_device(lua_State* L) {
intptr_t cb_ref = luaL_ref(L, LUA_REGISTRYINDEX);
server_get_info(_query_device_name_cb, (void*) cb_ref);
return 0;
}
/**
* Set a device as the default device.
* @function set_default_device
* @param name name of the new default device
*/
int pustule_set_default_device(lua_State* L) {
const char* device_name = luaL_checkstring(L, 1);
pa_operation* op = pa_context_set_default_sink(pactx, device_name, NULL, NULL);
pa_operation_unref(op);
return 0;
}
/**
* Suspend/resume the specified device.
* @function suspend_device
* @param idx device index
* @param suspend boolean indicating whether to suspend (true) or resume (false)
*/
int pustule_suspend_device(lua_State* L) {
uint32_t idx = luaL_checkinteger(L, 1);
bool suspend = lua_toboolean(L, 2);
pa_operation* op = pa_context_suspend_sink_by_index(pactx, idx, suspend, NULL, NULL);
pa_operation_unref(op);
return 0;
}
/* Lua log functions */
static void _lua_log(lua_State* L, int level) {
/* Format string */
int n = lua_gettop(L);
lua_getglobal(L, "string");
lua_getfield(L, -1, "format");
lua_remove(L, -2);
lua_rotate(L, 1, 1);
int res = lua_pcall(L, n, 1, 0);
if (check_pcall_error(L, res))
return;
/* Log the message */
const char* msg = luaL_checkstring(L, 1);
lua_Debug ar;
lua_getstack(L, 1, &ar);
lua_getinfo(L, "lS", &ar);
log_log(level, ar.short_src, ar.currentline, msg);
}
int lua_log_debug(lua_State* L) {
_lua_log(L, LOG_DEBUG);
return 0;
}
int lua_log_info(lua_State* L) {
_lua_log(L, LOG_INFO);
return 0;
}
int lua_log_warn(lua_State* L) {
_lua_log(L, LOG_WARN);
return 0;
}
int lua_log_error(lua_State* L) {
_lua_log(L, LOG_ERROR);
return 0;
}
/* Helpers */
static void _table_add_str(lua_State* L, const char* name, const char* value) {
lua_pushstring(L, name);
lua_pushstring(L, value);
lua_settable(L, -3);
}
static void _table_add_bool(lua_State* L, const char* name, bool value) {
lua_pushstring(L, name);
lua_pushboolean(L, value ? 1 : 0);
lua_settable(L, -3);
}
static void _table_add_double(lua_State* L, const char* name, double value) {
lua_pushstring(L, name);
lua_pushnumber(L, value);
lua_settable(L, -3);
}
static void _table_add_volume(lua_State* L, const pa_cvolume* vol) {
double v = ((double) pa_cvolume_avg(vol)) / PA_VOLUME_NORM;
double vdb = pa_sw_volume_to_dB(pa_cvolume_avg(vol));
_table_add_double(L, "volume", v);
_table_add_double(L, "volume_db", vdb);
}
static void _table_add_proplist(lua_State* L, pa_proplist* props) {
if (props && !pa_proplist_isempty(props)) {
void* state = NULL;
const char* key;
while ((key = pa_proplist_iterate(props, &state)) != NULL) {
const char* value = pa_proplist_gets(props, key);
_table_add_str(L, key, value);
}
}
}
/* Convert an input event to a Lua table */
void pustule_push_input_event(lua_State* L, pustule_input_event_t* iev) {
lua_newtable(L);
_table_add_str(L, "name", iev->name);
_table_add_str(L, "driver", iev->driver);
_table_add_bool(L, "muted", iev->mute);
_table_add_bool(L, "volume_writable", iev->volume_writable);
if (iev->has_volume)
_table_add_volume(L, &(iev->volume));
else
_table_add_bool(L, "volume", false);
_table_add_proplist(L, iev->proplist);
}
/* Convert a device event to a Lua table */
void pustule_push_device_event(lua_State* L, pustule_device_event_t* dev) {
lua_newtable(L);
_table_add_str(L, "name", dev->name);
_table_add_str(L, "description", dev->description);
_table_add_bool(L, "muted", dev->mute);
_table_add_volume(L, &(dev->volume));
_table_add_proplist(L, dev->proplist);
}
/* Handle listeners */
array_t* _listeners_by_name(const char* name) {
if (strcmp("input_added", name) == 0)
return &input_added_listeners;
if (strcmp("input_removed", name) == 0)
return &input_removed_listeners;
if (strcmp("device_added", name) == 0)
return &device_added_listeners;
if (strcmp("device_removed", name) == 0)
return &device_removed_listeners;
return NULL;
}
int pustule_connect_signal(lua_State* L) {
// type: (string signal_name, function callback) -> int
const char* signal_name = luaL_checkstring(L, 1);
log_debug("Connecting listener for signal '%s'", signal_name);
array_t* listeners = _listeners_by_name(signal_name);
if (!listeners) {
return luaL_error(L, "Invalid signal name: %s", signal_name);
}
int callback_ref = luaL_ref(L, LUA_REGISTRYINDEX);
array_push(listeners, callback_ref);
lua_pushinteger(L, callback_ref);
return 1;
}
int pustule_disconnect_signal(lua_State* L) {
// type: (string signal_name, int callback_ref) -> void
const char* signal_name = luaL_checkstring(L, 1);
log_debug("Disconnecting listener for signal '%s'", signal_name);
array_t* listeners = _listeners_by_name(signal_name);
if (!listeners) {
return luaL_error(L, "Invalid signal name: %s", signal_name);
}
int callback_ref = luaL_checkinteger(L, 2);
array_remove(listeners, callback_ref);
luaL_unref(L, LUA_REGISTRYINDEX, callback_ref);
return 0;
}
/* Register Lua module */
void init_lua_module(lua_State* L) {
g_L = L;
array_init(&input_added_listeners);
array_init(&input_removed_listeners);
array_init(&device_added_listeners);
array_init(&device_removed_listeners);
luaL_requiref(L, "pustule", luaopen_pustule, 1);
lua_pop(L, 1);
}
static const struct luaL_Reg pustule_lib[] = {
{"connect_signal", pustule_connect_signal},
{"disconnect_signal", pustule_disconnect_signal},
{"get_input_volume", pustule_get_input_volume},
{"set_input_volume", pustule_set_input_volume},
{"get_device_volume", pustule_get_device_volume},
{"set_device_volume", pustule_set_device_volume},
{"get_default_device", pustule_get_default_device},
{"set_default_device", pustule_set_default_device},
{"suspend_device", pustule_suspend_device},
{"log_debug", lua_log_debug},
{"log_info", lua_log_info},
{"log_warn", lua_log_warn},
{"log_error", lua_log_error},
{NULL, NULL},
};
int luaopen_pustule(lua_State* L) {
luaL_newlib(L, pustule_lib);
return 1;
}