diff --git a/doc/config.ld b/doc/config.ld index c990afc..f3babc2 100644 --- a/doc/config.ld +++ b/doc/config.ld @@ -1,13 +1,27 @@ -project = 'lgi-async-extra' -title = 'lgi-async-extra' -description = 'An asynchronous high(er)-level API wrapper for lua-lgi' +project = 'lua-libpulse-glib' +title = 'lua-libpulse-glib' +description = "Lua bindings for PulseAudio's libpulse, using the GLib Main Loop." + +full_description = [[ +While libpulse provides different event loop implementations, these bindings focus on the GLib comaptibility only. +Therefore, all asynchronous functions need to be executed inside a running GLib Main Loop. +For now, this loop has to run on GLib's default main context. Custom loop contexts are currently not supported. + +The majority of the API is callback-based asynchronous. Callbacks always receive any potential errors as their +first parameter, usually just the error message as string, or `nil` when there was no error. Additional parameters +may either be just a boolean, for operations that don't return data, or the actual queried data. + +All numeric indices (such as sink or source indices) are adjusted to be 1-based in typical Lua fashion. +This means that when comparing the output of calls like @{Context:get_sinks} to the output of tools like `pactl`, +indices will be off by one. +]] template = true format = 'discount' pretty = 'lua' -prettify_files = 'show' +-- prettify_files = 'show' backtick_references = false wrap = true no_space_before_args = true diff --git a/src/lua_libpulse_glib/callback.c b/src/lua_libpulse_glib/callback.c new file mode 100644 index 0000000..25eef27 --- /dev/null +++ b/src/lua_libpulse_glib/callback.c @@ -0,0 +1,51 @@ +#include "callback.h" + +#include "pulseaudio.h" + + +simple_callback_data* prepare_lua_callback(lua_State* L) { + // Prepare a new thread to run the callback with + lua_pushstring(L, LUA_PULSEAUDIO); + lua_rawget(L, LUA_REGISTRYINDEX); + lua_pushstring(L, LUA_PA_REGISTRY); + lua_gettable(L, -2); + lua_State* thread = lua_newthread(L); + int thread_ref = luaL_ref(L, -2); + + // Copy the callback function to the thread's stack + lua_pushvalue(L, 2); + lua_xmove(L, thread, 1); + + simple_callback_data* data = malloc(sizeof(struct simple_callback_data)); + data->L = thread; + data->thread_ref = thread_ref; + data->is_list = false; + + return data; +} + + +void free_lua_callback(simple_callback_data* data) { + lua_State* L = data->L; + + // Remove thread reference + lua_pushstring(L, LUA_PULSEAUDIO); + lua_rawget(L, LUA_REGISTRYINDEX); + lua_pushstring(L, LUA_PA_REGISTRY); + lua_gettable(L, -2); + luaL_unref(L, -1, data->thread_ref); + + free(data); +} + + +void success_callback(pa_context* c, int success, void* userdata) { + simple_callback_data* data = (simple_callback_data*) userdata; + lua_State* L = data->L; + + lua_pushnil(L); + lua_pushboolean(L, success); + lua_call(L, 2, 0); + + free_lua_callback(data); +} diff --git a/src/lua_libpulse_glib/callback.h b/src/lua_libpulse_glib/callback.h new file mode 100644 index 0000000..be9aa49 --- /dev/null +++ b/src/lua_libpulse_glib/callback.h @@ -0,0 +1,42 @@ +#ifndef callback_h_INCLUDED +#define callback_h_INCLUDED + + +#include +#include +#include + +typedef struct simple_callback_data { + lua_State* L; + int thread_ref; + // PulseAudio's introspection callbacks are used for operations that return single values, as well as operations + // that return a list. But the callback itself cannot know in which context it is called, so we have to provide + // that information explicitly. + bool is_list; +} simple_callback_data; + + +// Prepares a Lua thread that can call a Lua function as +// callback inside a PulseAudio callback. +// +// Assumes that the Lua function to call is at the top of the stack and copies it to the +// new thread. +// +// The thread will be kept in memory by a unique ref in the registry, so the callback has to +// `luaL_unref` that, to mark it for the garbage collector. +// +// The returned callback data needs to be `free()`d at the end of the callback. `free_lua_callback` +// handles both `free()` and `luaL_unref()`. +simple_callback_data* prepare_lua_callback(lua_State* L); + + +// Removes the thread reference, to allow the thread to be garbage collected, and `free`s +// the callback data. +void free_lua_callback(simple_callback_data* data); + + +// Simple implementation of `pa_context_success_cb_t` that calls a provided Lua function. +void success_callback(pa_context* c, int success, void* userdata); + +#endif // callback_h_INCLUDED + diff --git a/src/lua_libpulse_glib/context.c b/src/lua_libpulse_glib/context.c index 554237b..324a48f 100644 --- a/src/lua_libpulse_glib/context.c +++ b/src/lua_libpulse_glib/context.c @@ -1,12 +1,15 @@ - -#include "pulseaudio.h" #include "context.h" -#include "pulse/context.h" + +#include "callback.h" +#include "pulseaudio.h" + +#include +#include -void -context_state_callback(pa_context* c, void* userdata) -{ +/* Calls the user-provided callback with the updates state info. + */ +void context_state_callback(pa_context* c, void* userdata) { context_state_callback_data* data = (context_state_callback_data*) userdata; luaL_checktype(data->L, 1, LUA_TFUNCTION); luaL_checkudata(data->L, 2, LUA_PA_CONTEXT); @@ -23,9 +26,7 @@ context_state_callback(pa_context* c, void* userdata) } -int -context_new(lua_State* L, pa_mainloop_api* pa_api) -{ +int context_new(lua_State* L, pa_mainloop_api* pa_api) { const char* name = luaL_checkstring(L, -1); // TODO: libpulse recommends using `new_with_proplist` instead. But I need to figure out that `proplist` first. pa_context* ctx = pa_context_new(pa_api, name); @@ -33,7 +34,7 @@ context_new(lua_State* L, pa_mainloop_api* pa_api) return luaL_error(L, "failed to create pulseaudio context"); } - lua_pa_context* lgi_ctx = lua_newuserdata (L, sizeof(lua_pa_context)); + lua_pa_context* lgi_ctx = lua_newuserdata(L, sizeof(lua_pa_context)); if (lgi_ctx == NULL) { return luaL_error(L, "failed to create context userdata"); } @@ -48,19 +49,7 @@ context_new(lua_State* L, pa_mainloop_api* pa_api) } -int -context__index(lua_State* L) -{ - const char* index = luaL_checkstring(L, 2); - luaL_getmetatable(L, LUA_PA_CONTEXT); - lua_getfield(L, -1, index); - return 1; -} - - -int -context__gc(lua_State* L) -{ +int context__gc(lua_State* L) { lua_pa_context* ctx = luaL_checkudata(L, 1, LUA_PA_CONTEXT); if (ctx->connected == TRUE) { @@ -83,16 +72,14 @@ context__gc(lua_State* L) } -int -context_connect(lua_State* L) -{ +int context_connect(lua_State* L) { int nargs = lua_gettop(L); const char* server = NULL; if (lua_type(L, 2) == LUA_TSTRING) server = lua_tostring(L, 2); else if (lua_type(L, 2) != LUA_TNIL) { - const char *typearg; + const char* typearg; if (luaL_getmetafield(L, 2, "__name") == LUA_TSTRING) typearg = lua_tostring(L, -1); else if (lua_type(L, 2) == LUA_TLIGHTUSERDATA) @@ -141,11 +128,54 @@ context_connect(lua_State* L) } -int -context_get_state(lua_State* L) -{ +int context_disconnect(lua_State* L) { + lua_pa_context* ctx = luaL_checkudata(L, 1, LUA_PA_CONTEXT); + pa_context_disconnect(ctx->context); + return 0; +} + + +int context_get_state(lua_State* L) { lua_pa_context* ctx = luaL_checkudata(L, 1, LUA_PA_CONTEXT); pa_context_state_t state = pa_context_get_state(ctx->context); lua_pushinteger(L, state); return 1; } + + +int context_set_default_sink(lua_State* L) { + lua_pa_context* ctx = luaL_checkudata(L, 1, LUA_PA_CONTEXT); + const char* name = luaL_checkstring(L, 2); + + simple_callback_data* data = prepare_lua_callback(L); + + pa_operation* op = pa_context_set_default_sink(ctx->context, name, success_callback, data); + if (op == NULL) { + int error = pa_context_errno(ctx->context); + lua_pushvalue(L, 2); + lua_pushfstring(L, "failed to set default sink: %s", pa_strerror(error)); + lua_call(L, 1, 0); + return 0; + } + + return 0; +} + + +int context_set_default_source(lua_State* L) { + lua_pa_context* ctx = luaL_checkudata(L, 1, LUA_PA_CONTEXT); + const char* name = luaL_checkstring(L, 2); + + simple_callback_data* data = prepare_lua_callback(L); + + pa_operation* op = pa_context_set_default_source(ctx->context, name, success_callback, data); + if (op == NULL) { + int error = pa_context_errno(ctx->context); + lua_pushvalue(L, 2); + lua_pushfstring(L, "failed to set default source: %s", pa_strerror(error)); + lua_call(L, 1, 0); + return 0; + } + + return 0; +} diff --git a/src/lua_libpulse_glib/context.h b/src/lua_libpulse_glib/context.h index 76870b8..a68c071 100644 --- a/src/lua_libpulse_glib/context.h +++ b/src/lua_libpulse_glib/context.h @@ -1,11 +1,20 @@ +/** Bindings for libpulse's connection context. + * + * The connection @{Context} provides introspection calls to query state from the server and various commands to + * change this state. + * + * In many cases, sinks and sources may be addressed by either their name or their numeric index. + * Both can be queried using the `get_(sink|source)_info` or `get_(sink|source)s` calls. + * + * @module pulseaudio.context + */ #pragma once -#include "stdbool.h" -#include "lua.h" -#include "lauxlib.h" -#include "pulse/context.h" -#include "pulse/mainloop-api.h" -#include "introspection.h" +#include +#include +#include +#include +#include #define LUA_PA_CONTEXT "pulseaudio.context" @@ -23,22 +32,428 @@ typedef struct lua_pa_context { } lua_pa_context; -int -context_new(lua_State*, pa_mainloop_api*); -int -context__index(lua_State*); -int -context__gc(lua_State*); -int -context_connect(lua_State*); +int context_new(lua_State*, pa_mainloop_api*); +int context__gc(lua_State*); -static const struct luaL_Reg context_mt [] = { - {"__index", context__index}, +/// Context +/// @type Context + + +/** Connects the context to the given server address. + * + * If the server address is `nil`, libpulse will attempt to connect to what it considers the default server. + * In most cases, this is the local machine. + * + * The provided callback function will be registered as state callback function, and will be called whenever the + * context's connection state changes. + * + * @function Context:connect + * @async + * @tparam[opt=nil] string server_address The server address. + * @tparam function cb The connection state callback. + * @treturn[opt] string The error message + * @treturn string The state + */ +int context_connect(lua_State*); + +/** Disconnects from the server. + * + * @function Context:disconnect + */ +int context_disconnect(lua_State* L); + + +/** Gets information about the server the context is connected to. + * + * See [pa_server_info](https://freedesktop.org/software/pulseaudio/doxygen/structpa__server__info.html) + * for documentation on the return type. + * + * This will fail when the connection state is anything other than `READY`. + * + * @function Context:get_server_info + * @async + * @tparam function cb + * @return[opt] string The error + * @return table The server info. + */ +int context_get_server_info(lua_State*); + + +// Sinks + + +/** Gets information about the given sink. + * + * The sink may be indicated by either its name or its index. + * + * See [pa_sink_info](https://freedesktop.org/software/pulseaudio/doxygen/structpa__sink__info.html) + * for documentation on the return type. + * + * @function Context:get_sink_info + * @async + * @tparam number|string sink The index or name of the sink to query. + * @tparam function cb + * @treturn[opt] string + * @treturn table + */ +int context_get_sink_info(lua_State*); + +/** Gets information about all sinks. + * + * This returns the same information as @{Context:get_sink_info} would have returned for every sink + * that's currently registered at the server. + * + * See [pa_sink_info](https://freedesktop.org/software/pulseaudio/doxygen/structpa__sink__info.html) + * for documentation on the return type. + * + * @function Context:get_sinks + * @async + * @tparam function cb + * @treturn[opt] string + * @treturn table + */ +int context_get_sink_info_list(lua_State*); +int context_get_sink_info_by_name(lua_State*); +int context_get_sink_info_by_index(lua_State*); + +/** Sets the sink's volume to the given value. + * + * The sink may be indicated by either its name or its index. + * + * @function Context:set_sink_volume + * @async + * @tparam number|string sink The sink to update. + * @tparam Volume volume + * @tparam function cb + * @treturn[opt] string + * @treturn boolean + */ +int context_set_sink_volume(lua_State*); +int context_set_sink_volume_by_name(lua_State*); +int context_set_sink_volume_by_index(lua_State*); + +/** Sets the sink's mute state. + * + * The sink may be indicated by either its name or its index. + * + * @function Context:set_sink_mute + * @async + * @tparam number|string sink The sink to update. + * @tparam boolean mute + * @tparam function cb + * @treturn[opt] string + * @treturn boolean + */ +int context_set_sink_mute(lua_State*); +int context_set_sink_mute_by_name(lua_State*); +int context_set_sink_mute_by_index(lua_State*); + +/** Sets the sink's suspended state. + * + * The sink may be indicated by either its name or its index. + * + * @function Context:set_sink_suspended + * @async + * @tparam number|string sink The sink to update. + * @tparam boolean suspended + * @tparam function cb + * @treturn[opt] string + * @treturn boolean + */ +int context_set_sink_suspended(lua_State*); +int context_set_sink_suspended_by_name(lua_State*); +int context_set_sink_suspended_by_index(lua_State*); + + +// Sink Inputs + +/** Gets information about the given sink input. + * + * The sink input may be indicated by either its name or its index. + * + * See [pa_sink_input_info](https://freedesktop.org/software/pulseaudio/doxygen/structpa__sink_input__info.html) + * for documentation on the return type. + * + * @function Context:get_sink_input_info + * @async + * @tparam number|string sink input The index or name of the sink input to query. + * @tparam function cb + * @treturn[opt] string + * @treturn table + */ +int context_get_sink_input_info(lua_State*); + +/** Gets information about all sink inputs. + * + * This returns the same information as @{Context:get_sink_input_info} would have returned for every sink input + * that's currently registered at the server. + * + * See [pa_sink_input_info](https://freedesktop.org/software/pulseaudio/doxygen/structpa__sink_input__info.html) + * for documentation on the return type. + * + * @function Context:get_sink_inputs + * @async + * @tparam function cb + * @treturn[opt] string + * @treturn table + */ +int context_get_sink_input_info_list(lua_State*); + + +/** Moves the sink input to a different name. + * + * The target sink may be indicated by either its name or its index. + * + * @function Context:set_sink_suspended + * @async + * @tparam number The sink input to move. + * @tparam number|string sink The sink to update. + * @tparam function cb + * @treturn[opt] string + * @treturn boolean + */ +int context_move_sink_input(lua_State*); +int context_move_sink_input_by_name(lua_State*); +int context_move_sink_input_by_index(lua_State*); + +/** Sets the sink input's volume to the given value. + * + * @function Context:set_sink_input_volume + * @async + * @tparam number sink_input The sink input to update. + * @tparam Volume volume + * @tparam function cb + * @treturn[opt] string + * @treturn boolean + */ +int context_set_sink_input_volume(lua_State*); + +/** Sets the sink input's mute state. + * + * @function Context:set_sink_input_mute + * @async + * @tparam number sink_input The sink input to update. + * @tparam boolean mute + * @tparam function cb + * @treturn[opt] string + * @treturn boolean + */ +int context_set_sink_input_mute(lua_State*); + +/** Kills the sink input. + * + * @function Context:kill_sink_input + * @async + * @tparam number sink_input The sink input to kill. + * @tparam function cb + * @treturn[opt] string + * @treturn boolean + */ +int context_kill_sink_input(lua_State*); + + +// Sources + +/** Gets information about the given source. + * + * The source may be indicated by either its name or its index. + * + * See [pa_source_info](https://freedesktop.org/software/pulseaudio/doxygen/structpa__source__info.html) + * for documentation on the return type. + * + * @function Context:get_source_info + * @async + * @tparam number|string source The index or name of the source to query. + * @tparam function cb + * @treturn[opt] string + * @treturn table + */ +int context_get_source_info(lua_State*); + +/** Gets information about all sources. + * + * This returns the same information as @{Context:get_source_info} would have returned for every source + * that's currently registered at the server. + * + * See [pa_source_info](https://freedesktop.org/software/pulseaudio/doxygen/structpa__source__info.html) + * for documentation on the return type. + * + * @function Context:get_sources + * @async + * @tparam function cb + * @treturn[opt] string + * @treturn table + */ +int context_get_source_info_list(lua_State*); +int context_get_source_info_by_name(lua_State*); +int context_get_source_info_by_index(lua_State*); + +/** Sets the source's volume to the given value. + * + * The source may be indicated by either its name or its index. + * + * @function Context:set_source_volume + * @async + * @tparam number|string source The source to update. + * @tparam Volume volume + * @tparam function cb + * @treturn[opt] string + * @treturn boolean + */ +int context_set_source_volume(lua_State*); +int context_set_source_volume_by_name(lua_State*); +int context_set_source_volume_by_index(lua_State*); + +/** Sets the source's mute state. + * + * The source may be indicated by either its name or its index. + * + * @function Context:set_source_mute + * @async + * @tparam number|string source The source to update. + * @tparam boolean mute + * @tparam function cb + * @treturn[opt] string + * @treturn boolean + */ +int context_set_source_mute(lua_State*); +int context_set_source_mute_by_name(lua_State*); +int context_set_source_mute_by_index(lua_State*); + +/** Sets the source's suspended state. + * + * The source may be indicated by either its name or its index. + * + * @function Context:set_source_suspended + * @async + * @tparam number|string source The source to update. + * @tparam boolean suspended + * @tparam function cb + * @treturn[opt] string + * @treturn boolean + */ +int context_set_source_suspended(lua_State*); +int context_set_source_suspended_by_name(lua_State*); +int context_set_source_suspended_by_index(lua_State*); + + +// Source Outputs + +/** Gets information about the given source output. + * + * See [pa_source_output_info](https://freedesktop.org/software/pulseaudio/doxygen/structpa__source_output__info.html) + * for documentation on the return type. + * + * @function Context:get_source_output_info + * @async + * @tparam number source_output The index of the source output to query. + * @tparam function cb + * @treturn[opt] string + * @treturn table + */ +int context_get_source_output_info(lua_State*); + +/** Gets information about all source outputs. + * + * This returns the same information as @{Context:get_source_output_info} would have returned for every source output + * that's currently registered at the server. + * + * See [pa_source_output_info](https://freedesktop.org/software/pulseaudio/doxygen/structpa__source_output__info.html) + * for documentation on the return type. + * + * @function Context:get_source_outputs + * @async + * @tparam function cb + * @treturn[opt] string + * @treturn table + */ +int context_get_source_output_info_list(lua_State*); + +/** Moves the source output to a different name. + * + * The target source may be indicated by either its name or its index. + * + * @function Context:set_source_suspended + * @async + * @tparam number The source output to move. + * @tparam number|string source The source to update. + * @tparam function cb + * @treturn[opt] string + * @treturn boolean + */ +int context_move_source_output(lua_State*); +int context_move_source_output_by_name(lua_State*); +int context_move_source_output_by_index(lua_State*); + + +/** Sets the source output's volume to the given value. + * + * @function Context:set_source_output_volume + * @async + * @tparam number source_output The source output to update. + * @tparam Volume volume + * @tparam function cb + * @treturn[opt] string + * @treturn boolean + */ +int context_set_source_output_volume(lua_State*); + +/** Sets the source output's mute state. + * + * @function Context:set_source_output_mute + * @async + * @tparam number source_output The source output to update. + * @tparam boolean mute + * @tparam function cb + * @treturn[opt] string + * @treturn boolean + */ +int context_set_source_output_mute(lua_State*); + +/** Kills the source output. + * + * @function Context:kill_source_output + * @async + * @tparam number source_output The source output to kill. + * @tparam function cb + * @treturn[opt] string + * @treturn boolean + */ +int context_kill_source_output(lua_State*); + + +static const struct luaL_Reg context_mt[] = { {"__gc", context__gc}, - {"connect", context_connect}, - {"get_server_info", context_get_server_info}, - {"get_sinks", context_get_sink_info_list}, - {"get_sources", context_get_source_info_list}, - {NULL, NULL} + { NULL, NULL } +}; + + +static const struct luaL_Reg context_f[] = { + {"connect", context_connect }, + { "disconnect", context_disconnect }, + { "get_server_info", context_get_server_info }, + { "get_sinks", context_get_sink_info_list }, + { "get_sink_info", context_get_sink_info }, + { "set_sink_volume", context_set_sink_volume }, + { "set_sink_mute", context_set_sink_mute }, + { "set_sink_suspended", context_set_sink_suspended }, + { "get_sink_inputs", context_get_sink_input_info_list }, + { "get_sink_input_info", context_get_sink_input_info }, + { "set_sink_input_volume", context_set_sink_input_volume }, + { "set_sink_input_mute", context_set_sink_input_mute }, + { "move_sink_input", context_move_sink_input }, + { "kill_sink_input", context_kill_sink_input }, + { "set_source_volume", context_set_source_volume }, + { "set_source_mute", context_set_source_mute }, + { "set_source_suspended", context_set_source_suspended }, + { "get_sources", context_get_source_info_list }, + { "get_source_info", context_get_source_info }, + { "get_source_outputs", context_get_source_output_info_list}, + { "get_source_output_info", context_get_source_output_info }, + { "set_source_output_volume", context_set_source_output_volume }, + { "set_source_output_mute", context_set_source_output_mute }, + { "move_source_output", context_move_source_output }, + { "kill_source_output", context_kill_source_output }, + { NULL, NULL } }; diff --git a/src/lua_libpulse_glib/convert.c b/src/lua_libpulse_glib/convert.c new file mode 100644 index 0000000..203ca81 --- /dev/null +++ b/src/lua_libpulse_glib/convert.c @@ -0,0 +1,532 @@ +#include "convert.h" + +#include "proplist.h" +#include "volume.h" + +#include + + +void channel_map_to_lua(lua_State* L, const pa_channel_map* spec) { + lua_createtable(L, spec->channels, 0); + int table_index = lua_gettop(L); + + for (int i = 0; i < spec->channels; ++i) { + lua_pushinteger(L, i + 1); + lua_pushinteger(L, spec->map[i]); + lua_settable(L, table_index); + } +} + + +void sample_spec_to_lua(lua_State* L, const pa_sample_spec* spec) { + lua_createtable(L, 0, 3); + int table_index = lua_gettop(L); + + lua_pushstring(L, "rate"); + lua_pushinteger(L, spec->rate); + lua_settable(L, table_index); + + lua_pushstring(L, "channels"); + lua_pushinteger(L, spec->channels); + lua_settable(L, table_index); + + lua_pushstring(L, "format"); + lua_pushinteger(L, spec->format); + lua_settable(L, table_index); +} + + +void sink_port_info_to_lua(lua_State* L, const pa_sink_port_info* info) { + lua_createtable(L, 0, 6); + int table_index = lua_gettop(L); + + lua_pushstring(L, "name"); + lua_pushstring(L, info->name); + lua_settable(L, table_index); + + lua_pushstring(L, "description"); + lua_pushstring(L, info->description); + lua_settable(L, table_index); + + lua_pushstring(L, "priority"); + lua_pushinteger(L, info->priority); + lua_settable(L, table_index); + + lua_pushstring(L, "available"); + lua_pushinteger(L, info->available); + lua_settable(L, table_index); + + lua_pushstring(L, "availability_group"); + lua_pushstring(L, info->availability_group); + lua_settable(L, table_index); + + lua_pushstring(L, "type"); + lua_pushinteger(L, info->type); + lua_settable(L, table_index); +} + + +void source_port_info_to_lua(lua_State* L, const pa_source_port_info* info) { + lua_createtable(L, 0, 6); + int table_index = lua_gettop(L); + + lua_pushstring(L, "name"); + lua_pushstring(L, info->name); + lua_settable(L, table_index); + + lua_pushstring(L, "description"); + lua_pushstring(L, info->description); + lua_settable(L, table_index); + + lua_pushstring(L, "priority"); + lua_pushinteger(L, info->priority); + lua_settable(L, table_index); + + lua_pushstring(L, "available"); + lua_pushinteger(L, info->available); + lua_settable(L, table_index); + + lua_pushstring(L, "availability_group"); + lua_pushstring(L, info->availability_group); + lua_settable(L, table_index); + + lua_pushstring(L, "type"); + lua_pushinteger(L, info->type); + lua_settable(L, table_index); +} + + +void format_info_to_lua(lua_State* L, const pa_format_info* info) { + lua_createtable(L, 0, 2); + int table_index = lua_gettop(L); + + lua_pushstring(L, "encoding"); + lua_pushinteger(L, info->encoding); + lua_settable(L, table_index); + + lua_pushstring(L, "plist"); + proplist_to_lua(L, pa_proplist_copy(info->plist)); + lua_settable(L, table_index); +} + + +void sink_ports_to_lua(lua_State* L, pa_sink_port_info** list, int n_ports, pa_sink_port_info* active) { + lua_createtable(L, n_ports, 1); + int table_index = lua_gettop(L); + + for (int i = 0; i < n_ports; ++i) { + lua_pushinteger(L, i + 1); + sink_port_info_to_lua(L, list[i]); + + if (list[i] == active) { + lua_pushstring(L, "active"); + lua_pushvalue(L, -2); + lua_settable(L, table_index); + } + + lua_settable(L, table_index); + } +} + + +void source_ports_to_lua(lua_State* L, pa_source_port_info** list, int n_ports, pa_source_port_info* active) { + lua_createtable(L, n_ports, 1); + int table_index = lua_gettop(L); + + for (int i = 0; i < n_ports; ++i) { + lua_pushinteger(L, i + 1); + source_port_info_to_lua(L, list[i]); + + if (list[i] == active) { + lua_pushstring(L, "active"); + lua_pushvalue(L, -2); + lua_settable(L, table_index); + } + + lua_settable(L, table_index); + } +} + + +void formats_to_lua(lua_State* L, pa_format_info** list, int n_formats) { + lua_createtable(L, n_formats, 0); + int table_index = lua_gettop(L); + + for (int i = 0; i < n_formats; ++i) { + lua_pushinteger(L, i + 1); + format_info_to_lua(L, list[i]); + lua_settable(L, table_index); + } +} + + +void sink_info_to_lua(lua_State* L, const pa_sink_info* info) { + lua_createtable(L, 0, 24); + int table_index = lua_gettop(L); + + lua_pushstring(L, "name"); + lua_pushstring(L, info->name); + lua_settable(L, table_index); + + lua_pushstring(L, "index"); + // Convert C's 0-based index to Lua's 1-base + lua_pushinteger(L, info->index + 1); + lua_settable(L, table_index); + + lua_pushstring(L, "description"); + lua_pushstring(L, info->description); + lua_settable(L, table_index); + + lua_pushstring(L, "sample_spec"); + sample_spec_to_lua(L, &info->sample_spec); + lua_settable(L, table_index); + + lua_pushstring(L, "channel_map"); + channel_map_to_lua(L, &info->channel_map); + lua_settable(L, table_index); + + lua_pushstring(L, "owner_module"); + lua_pushinteger(L, info->owner_module); + lua_settable(L, table_index); + + lua_pushstring(L, "volume"); + volume_to_lua(L, &info->volume); + lua_settable(L, table_index); + + lua_pushstring(L, "mute"); + lua_pushinteger(L, info->mute); + lua_settable(L, table_index); + + lua_pushstring(L, "monitor_source"); + lua_pushinteger(L, info->monitor_source); + lua_settable(L, table_index); + + lua_pushstring(L, "monitor_source_name"); + lua_pushstring(L, info->monitor_source_name); + lua_settable(L, table_index); + + lua_pushstring(L, "latency"); + lua_pushinteger(L, info->latency); + lua_settable(L, table_index); + + lua_pushstring(L, "driver"); + lua_pushstring(L, info->driver); + lua_settable(L, table_index); + + lua_pushstring(L, "flags"); + lua_pushinteger(L, info->flags); + lua_settable(L, table_index); + + lua_pushstring(L, "proplist"); + proplist_to_lua(L, pa_proplist_copy(info->proplist)); + lua_settable(L, table_index); + + lua_pushstring(L, "base_volume"); + lua_pushinteger(L, info->base_volume); + lua_settable(L, table_index); + + lua_pushstring(L, "state"); + lua_pushinteger(L, info->state); + lua_settable(L, table_index); + + lua_pushstring(L, "n_volume_steps"); + lua_pushinteger(L, info->n_volume_steps); + lua_settable(L, table_index); + + lua_pushstring(L, "card"); + lua_pushinteger(L, info->card); + lua_settable(L, table_index); + + lua_pushstring(L, "ports"); + sink_ports_to_lua(L, info->ports, info->n_ports, info->active_port); + lua_settable(L, table_index); + + lua_pushstring(L, "formats"); + formats_to_lua(L, info->formats, info->n_formats); + lua_settable(L, table_index); +} + + +void source_info_to_lua(lua_State* L, const pa_source_info* info) { + lua_createtable(L, 0, 24); + int table_index = lua_gettop(L); + + lua_pushstring(L, "name"); + lua_pushstring(L, info->name); + lua_settable(L, table_index); + + lua_pushstring(L, "index"); + // Convert C's 0-based index to Lua's 1-base + lua_pushinteger(L, info->index + 1); + lua_settable(L, table_index); + + lua_pushstring(L, "description"); + lua_pushstring(L, info->description); + lua_settable(L, table_index); + + lua_pushstring(L, "sample_spec"); + sample_spec_to_lua(L, &info->sample_spec); + lua_settable(L, table_index); + + lua_pushstring(L, "channel_map"); + channel_map_to_lua(L, &info->channel_map); + lua_settable(L, table_index); + + lua_pushstring(L, "owner_module"); + lua_pushinteger(L, info->owner_module); + lua_settable(L, table_index); + + lua_pushstring(L, "volume"); + volume_to_lua(L, &info->volume); + lua_settable(L, table_index); + + lua_pushstring(L, "mute"); + lua_pushinteger(L, info->mute); + lua_settable(L, table_index); + + lua_pushstring(L, "monitor_of_sink"); + lua_pushinteger(L, info->monitor_of_sink); + lua_settable(L, table_index); + + lua_pushstring(L, "monitor_of_sink_name"); + lua_pushstring(L, info->monitor_of_sink_name); + lua_settable(L, table_index); + + lua_pushstring(L, "latency"); + lua_pushinteger(L, info->latency); + lua_settable(L, table_index); + + lua_pushstring(L, "driver"); + lua_pushstring(L, info->driver); + lua_settable(L, table_index); + + lua_pushstring(L, "flags"); + lua_pushinteger(L, info->flags); + lua_settable(L, table_index); + + lua_pushstring(L, "proplist"); + proplist_to_lua(L, pa_proplist_copy(info->proplist)); + lua_settable(L, table_index); + + lua_pushstring(L, "configured_latency"); + lua_pushinteger(L, info->configured_latency); + lua_settable(L, table_index); + + lua_pushstring(L, "base_volume"); + lua_pushinteger(L, info->base_volume); + lua_settable(L, table_index); + + lua_pushstring(L, "state"); + lua_pushinteger(L, info->state); + lua_settable(L, table_index); + + lua_pushstring(L, "n_volume_steps"); + lua_pushinteger(L, info->n_volume_steps); + lua_settable(L, table_index); + + lua_pushstring(L, "card"); + lua_pushinteger(L, info->card); + lua_settable(L, table_index); + + lua_pushstring(L, "ports"); + source_ports_to_lua(L, info->ports, info->n_ports, info->active_port); + lua_settable(L, table_index); + + lua_pushstring(L, "formats"); + formats_to_lua(L, info->formats, info->n_formats); + lua_settable(L, table_index); +} + + +void server_info_to_lua(lua_State* L, const pa_server_info* info) { + lua_createtable(L, 0, 9); + int table_index = lua_gettop(L); + + lua_pushstring(L, "user_name"); + lua_pushstring(L, info->user_name); + lua_settable(L, table_index); + + lua_pushstring(L, "host_name"); + lua_pushstring(L, info->host_name); + lua_settable(L, table_index); + + lua_pushstring(L, "server_version"); + lua_pushstring(L, info->server_version); + lua_settable(L, table_index); + + lua_pushstring(L, "server_name"); + lua_pushstring(L, info->server_name); + lua_settable(L, table_index); + + lua_pushstring(L, "default_sink_name"); + lua_pushstring(L, info->default_sink_name); + lua_settable(L, table_index); + + lua_pushstring(L, "default_source_name"); + lua_pushstring(L, info->default_source_name); + lua_settable(L, table_index); + + lua_pushstring(L, "cookie"); + lua_pushinteger(L, info->cookie); + lua_settable(L, table_index); + + lua_pushstring(L, "sample_spec"); + sample_spec_to_lua(L, &info->sample_spec); + lua_settable(L, table_index); + + lua_pushstring(L, "channel_map"); + channel_map_to_lua(L, &info->channel_map); + lua_settable(L, table_index); +} + + +void sink_input_info_to_lua(lua_State* L, const pa_sink_input_info* info) { + lua_createtable(L, 0, 15); + int table_index = lua_gettop(L); + + lua_pushstring(L, "index"); + lua_pushinteger(L, info->index + 1); + lua_settable(L, table_index); + + lua_pushstring(L, "name"); + lua_pushstring(L, info->name); + lua_settable(L, table_index); + + lua_pushstring(L, "owner_module"); + lua_pushinteger(L, info->owner_module); + lua_settable(L, table_index); + + lua_pushstring(L, "client"); + lua_pushinteger(L, info->client); + lua_settable(L, table_index); + + lua_pushstring(L, "sink"); + lua_pushinteger(L, info->sink); + lua_settable(L, table_index); + + lua_pushstring(L, "sample_spec"); + sample_spec_to_lua(L, &info->sample_spec); + lua_settable(L, table_index); + + lua_pushstring(L, "channel_map"); + channel_map_to_lua(L, &info->channel_map); + lua_settable(L, table_index); + + lua_pushstring(L, "has_volume"); + lua_pushboolean(L, info->has_volume); + lua_settable(L, table_index); + + lua_pushstring(L, "volume_writable"); + lua_pushboolean(L, info->volume_writable); + lua_settable(L, table_index); + + if (info->has_volume) { + lua_pushstring(L, "volume"); + volume_to_lua(L, &info->volume); + lua_settable(L, table_index); + } + + lua_pushstring(L, "buffer_usec"); + lua_pushinteger(L, info->buffer_usec); + lua_settable(L, table_index); + + lua_pushstring(L, "sink_usec"); + lua_pushinteger(L, info->sink_usec); + lua_settable(L, table_index); + + lua_pushstring(L, "resample_method"); + lua_pushstring(L, info->resample_method); + lua_settable(L, table_index); + + lua_pushstring(L, "driver"); + lua_pushstring(L, info->driver); + lua_settable(L, table_index); + + lua_pushstring(L, "mute"); + lua_pushboolean(L, info->mute); + lua_settable(L, table_index); + + lua_pushstring(L, "format"); + format_info_to_lua(L, info->format); + lua_settable(L, table_index); + + lua_pushstring(L, "proplist"); + proplist_to_lua(L, pa_proplist_copy(info->proplist)); + lua_settable(L, table_index); +} + + +void source_output_info_to_lua(lua_State* L, const pa_source_output_info* info) { + lua_createtable(L, 0, 15); + int table_index = lua_gettop(L); + + lua_pushstring(L, "index"); + lua_pushinteger(L, info->index + 1); + lua_settable(L, table_index); + + lua_pushstring(L, "name"); + lua_pushstring(L, info->name); + lua_settable(L, table_index); + + lua_pushstring(L, "owner_module"); + lua_pushinteger(L, info->owner_module); + lua_settable(L, table_index); + + lua_pushstring(L, "client"); + lua_pushinteger(L, info->client); + lua_settable(L, table_index); + + lua_pushstring(L, "source"); + lua_pushinteger(L, info->source); + lua_settable(L, table_index); + + lua_pushstring(L, "sample_spec"); + sample_spec_to_lua(L, &info->sample_spec); + lua_settable(L, table_index); + + lua_pushstring(L, "channel_map"); + channel_map_to_lua(L, &info->channel_map); + lua_settable(L, table_index); + + lua_pushstring(L, "has_volume"); + lua_pushboolean(L, info->has_volume); + lua_settable(L, table_index); + + lua_pushstring(L, "volume_writable"); + lua_pushboolean(L, info->volume_writable); + lua_settable(L, table_index); + + if (info->has_volume) { + lua_pushstring(L, "volume"); + volume_to_lua(L, &info->volume); + lua_settable(L, table_index); + } + + lua_pushstring(L, "buffer_usec"); + lua_pushinteger(L, info->buffer_usec); + lua_settable(L, table_index); + + lua_pushstring(L, "source_usec"); + lua_pushinteger(L, info->source_usec); + lua_settable(L, table_index); + + lua_pushstring(L, "resample_method"); + lua_pushstring(L, info->resample_method); + lua_settable(L, table_index); + + lua_pushstring(L, "driver"); + lua_pushstring(L, info->driver); + lua_settable(L, table_index); + + lua_pushstring(L, "mute"); + lua_pushboolean(L, info->mute); + lua_settable(L, table_index); + + lua_pushstring(L, "format"); + format_info_to_lua(L, info->format); + lua_settable(L, table_index); + + lua_pushstring(L, "proplist"); + proplist_to_lua(L, pa_proplist_copy(info->proplist)); + lua_settable(L, table_index); +} diff --git a/src/lua_libpulse_glib/convert.h b/src/lua_libpulse_glib/convert.h new file mode 100644 index 0000000..f4ac1fa --- /dev/null +++ b/src/lua_libpulse_glib/convert.h @@ -0,0 +1,20 @@ +#pragma once + +#include +#include +#include + + +void channel_map_to_lua(lua_State*, const pa_channel_map*); +void sample_spec_to_lua(lua_State*, const pa_sample_spec*); +void sink_port_info_to_lua(lua_State*, const pa_sink_port_info*); +void source_port_info_to_lua(lua_State*, const pa_source_port_info*); +void format_info_to_lua(lua_State*, const pa_format_info*); +void sink_ports_to_lua(lua_State*, pa_sink_port_info**, int, pa_sink_port_info*); +void source_ports_to_lua(lua_State*, pa_source_port_info**, int, pa_source_port_info*); +void formats_to_lua(lua_State*, pa_format_info**, int); +void sink_info_to_lua(lua_State*, const pa_sink_info*); +void source_info_to_lua(lua_State*, const pa_source_info*); +void server_info_to_lua(lua_State*, const pa_server_info*); +void sink_input_info_to_lua(lua_State*, const pa_sink_input_info*); +void source_output_info_to_lua(lua_State*, const pa_source_output_info*); diff --git a/src/lua_libpulse_glib/introspection.c b/src/lua_libpulse_glib/introspection.c index b92dc13..3ac4b9e 100644 --- a/src/lua_libpulse_glib/introspection.c +++ b/src/lua_libpulse_glib/introspection.c @@ -1,78 +1,30 @@ -#include -#include -#include "introspection.h" +#include "callback.h" #include "context.h" +#include "convert.h" +#include "proplist.h" #include "pulseaudio.h" +#include "volume.h" + +#include +#include +#include +#include +#include -// TODO: Figure out error handling for callbacks - - -typedef struct server_info_callback_data { - lua_State* L; - int thread_ref; -} server_info_callback_data; - - -void -server_info_to_lua(lua_State* L, const pa_server_info* info) -{ - lua_createtable(L, 0, 9); - int table_index = lua_gettop(L); - - lua_pushstring(L, "user_name"); - lua_pushstring(L, info->user_name); - lua_settable(L, table_index); - - lua_pushstring(L, "host_name"); - lua_pushstring(L, info->host_name); - lua_settable(L, table_index); - - lua_pushstring(L, "server_version"); - lua_pushstring(L, info->server_version); - lua_settable(L, table_index); - - lua_pushstring(L, "server_name"); - lua_pushstring(L, info->server_name); - lua_settable(L, table_index); - - lua_pushstring(L, "default_sink_name"); - lua_pushstring(L, info->default_sink_name); - lua_settable(L, table_index); - - lua_pushstring(L, "cookie"); - lua_pushinteger(L, info->cookie); - lua_settable(L, table_index); - - // TODO: Handle `sample_spec` and `channel_map` tables -} - - -void -server_info_callback(pa_context* c, const pa_server_info* info, void* userdata) -{ - - server_info_callback_data* data = (server_info_callback_data*) userdata; +void server_info_callback(pa_context* c, const pa_server_info* info, void* userdata) { + simple_callback_data* data = (simple_callback_data*) userdata; lua_State* L = data->L; lua_pushnil(L); server_info_to_lua(L, info); lua_call(L, 2, 0); - // Remove thread reference - lua_pushstring(L, LUA_PULSEAUDIO); - lua_rawget(L, LUA_REGISTRYINDEX); - lua_pushstring(L, LUA_PA_REGISTRY); - lua_gettable(L, -2); - luaL_unref(L, -1, data->thread_ref); - - free(data); + free_lua_callback(data); } -int -context_get_server_info(lua_State* L) -{ +int context_get_server_info(lua_State* L) { lua_pa_context* ctx = luaL_checkudata(L, 1, LUA_PA_CONTEXT); if (pa_context_get_state(ctx->context) != PA_CONTEXT_READY) { @@ -82,21 +34,7 @@ context_get_server_info(lua_State* L) return 0; } - // Prepare a new thread to run the callback with - lua_pushstring(L, LUA_PULSEAUDIO); - lua_rawget(L, LUA_REGISTRYINDEX); - lua_pushstring(L, LUA_PA_REGISTRY); - lua_gettable(L, -2); - lua_State* thread = lua_newthread(L); - int thread_ref = luaL_ref(L, -2); - - // Copy the callback function to the thread's stack - lua_pushvalue(L, 2); - lua_xmove(L, thread, 1); - - server_info_callback_data* data = calloc(1, sizeof(struct server_info_callback_data)); - data->L = thread; - data->thread_ref = thread_ref; + simple_callback_data* data = prepare_lua_callback(L); pa_operation* op = pa_context_get_server_info(ctx->context, server_info_callback, data); if (op == NULL) { @@ -111,58 +49,42 @@ context_get_server_info(lua_State* L) } -typedef struct sink_info_list_callback_data { - lua_State* L; - int thread_ref; -} sink_info_list_callback_data; - - -void -sink_info_to_lua(lua_State* L, const pa_sink_info* info) -{ - lua_createtable(L, 0, 24); - int table_index = lua_gettop(L); - - lua_pushstring(L, "name"); - lua_pushstring(L, info->name); - lua_settable(L, table_index); - - // TODO: Handle rest of the table -} - - -void -sink_info_list_callback(pa_context* c, const pa_sink_info* info, int eol, void* userdata) -{ - sink_info_list_callback_data* data = (sink_info_list_callback_data*) userdata; +void sink_info_callback(pa_context* c, const pa_sink_info* info, int eol, void* userdata) { + simple_callback_data* data = (simple_callback_data*) userdata; lua_State* L = data->L; - if (eol == 0) { - int i = lua_objlen(L, 2); - lua_pushinteger(L, i+1); - sink_info_to_lua(L, info); - lua_settable(L, 2); + if (data->is_list) { + if (!eol) { + int i = lua_objlen(L, 2); + lua_pushinteger(L, i + 1); + sink_info_to_lua(L, info); + lua_settable(L, 2); + } else { + // Insert the error argument + lua_pushnil(L); + lua_insert(L, -2); + + lua_call(L, 2, 0); + + free_lua_callback(data); + } } else { - // Insert the error argument - lua_pushnil(L); - lua_insert(L, -2); + if (!eol) { + lua_pushfstring(L, "only one sink info expected, but got multiple"); + lua_call(L, 1, 0); + } else { + lua_pushnil(L); + sink_info_to_lua(L, info); - lua_call(L, 2, 0); + lua_call(L, 2, 0); - lua_pushstring(L, LUA_PULSEAUDIO); - lua_rawget(L, LUA_REGISTRYINDEX); - - lua_pushstring(L, LUA_PA_REGISTRY); - lua_gettable(L, -2); - luaL_unref(L, -1, data->thread_ref); - free(data); + free_lua_callback(data); + } } } -int -context_get_sink_info_list(lua_State* L) -{ +int context_get_sink_info_list(lua_State* L) { lua_pa_context* ctx = luaL_checkudata(L, 1, LUA_PA_CONTEXT); if (pa_context_get_state(ctx->context) != PA_CONTEXT_READY) { @@ -172,82 +94,96 @@ context_get_sink_info_list(lua_State* L) return 0; } - // Prepare a new thread to run the callback with - lua_pushstring(L, LUA_PULSEAUDIO); - lua_rawget(L, LUA_REGISTRYINDEX); - lua_pushstring(L, LUA_PA_REGISTRY); - lua_gettable(L, -2); - lua_State* thread = lua_newthread(L); - int thread_ref = luaL_ref(L, -2); + simple_callback_data* data = prepare_lua_callback(L); + data->is_list = true; + // Create the list to store infos in + lua_newtable(data->L); - // Copy the callback function to the thread's stack - lua_pushvalue(L, 2); - lua_xmove(L, thread, 1); + pa_operation* op = pa_context_get_sink_info_list(ctx->context, sink_info_callback, data); + if (op == NULL) { + int error = pa_context_errno(ctx->context); + lua_pushvalue(L, 2); + lua_pushfstring(L, "failed to get sink info list: %s", pa_strerror(error)); + lua_call(L, 1, 0); + return 0; + } - // List to store the values in - lua_newtable(thread); - - sink_info_list_callback_data* data = calloc(1, sizeof(struct sink_info_list_callback_data)); - data->L = thread; - data->thread_ref = thread_ref; - - pa_context_get_sink_info_list(ctx->context, sink_info_list_callback, data); return 0; } -typedef struct source_info_list_callback_data { - lua_State* L; - int thread_ref; -} source_info_list_callback_data; +int context_get_sink_info_by_name(lua_State* L) { + lua_pa_context* ctx = luaL_checkudata(L, 1, LUA_PA_CONTEXT); + const char* name = luaL_checkstring(L, 2); + if (pa_context_get_state(ctx->context) != PA_CONTEXT_READY) { + lua_pushvalue(L, 2); + lua_pushstring(L, "connection not ready"); + lua_call(L, 1, 0); + return 0; + } -void -source_info_to_lua(lua_State* L, const pa_source_info* info) -{ - lua_createtable(L, 0, 24); - int table_index = lua_gettop(L); + simple_callback_data* data = prepare_lua_callback(L); - lua_pushstring(L, "name"); - lua_pushstring(L, info->name); - lua_settable(L, table_index); + pa_operation* op = pa_context_get_sink_info_by_name(ctx->context, name, sink_info_callback, data); + if (op == NULL) { + int error = pa_context_errno(ctx->context); + lua_pushvalue(L, 2); + lua_pushfstring(L, "failed to get sink info by name: %s", pa_strerror(error)); + lua_call(L, 1, 0); + return 0; + } - // TODO: Handle rest of the table + return 0; } -void -source_info_list_callback(pa_context* c, const pa_source_info* info, int eol, void* userdata) -{ - source_info_list_callback_data* data = (source_info_list_callback_data*) userdata; - lua_State* L = data->L; +int context_get_sink_info_by_index(lua_State* L) { + lua_pa_context* ctx = luaL_checkudata(L, 1, LUA_PA_CONTEXT); + lua_Integer index = luaL_checkinteger(L, 2); + if (index < 1) { + return luaL_error(L, "Sink index out of bounds. Got %d", index); + } - if (eol == 0) { - int i = lua_objlen(L, 2); - lua_pushinteger(L, i+1); - source_info_to_lua(L, info); - lua_settable(L, 2); - } else { - // Insert the error argument - lua_pushnil(L); - lua_insert(L, -2); + if (pa_context_get_state(ctx->context) != PA_CONTEXT_READY) { + lua_pushvalue(L, 2); + lua_pushstring(L, "connection not ready"); + lua_call(L, 1, 0); + return 0; + } - lua_call(L, 2, 0); + simple_callback_data* data = prepare_lua_callback(L); - lua_pushstring(L, LUA_PULSEAUDIO); - lua_rawget(L, LUA_REGISTRYINDEX); + pa_operation* op = pa_context_get_sink_info_by_index(ctx->context, (uint32_t) index - 1, sink_info_callback, data); + if (op == NULL) { + int error = pa_context_errno(ctx->context); + lua_pushvalue(L, 2); + lua_pushfstring(L, "failed to get sink info by index: %s", pa_strerror(error)); + lua_call(L, 1, 0); + return 0; + } - lua_pushstring(L, LUA_PA_REGISTRY); - lua_gettable(L, -2); - luaL_unref(L, -1, data->thread_ref); - free(data); + return 0; +} + + +int context_get_sink_info(lua_State* L) { + switch (lua_type(L, 2)) { + case LUA_TSTRING: { + return context_get_sink_info_by_name(L); + } + case LUA_TNUMBER: { + return context_get_sink_info_by_index(L); + } + default: { + lua_pushfstring(L, "expected number or string, got %s", luaL_typename(L, 2)); + return luaL_argerror(L, 2, lua_tostring(L, -1)); + } } } -int -context_get_source_info_list(lua_State* L) -{ +int context_set_sink_volume_by_name(lua_State* L) { lua_pa_context* ctx = luaL_checkudata(L, 1, LUA_PA_CONTEXT); if (pa_context_get_state(ctx->context) != PA_CONTEXT_READY) { @@ -257,25 +193,1116 @@ context_get_source_info_list(lua_State* L) return 0; } - // Prepare a new thread to run the callback with - lua_pushstring(L, LUA_PULSEAUDIO); - lua_rawget(L, LUA_REGISTRYINDEX); - lua_pushstring(L, LUA_PA_REGISTRY); - lua_gettable(L, -2); - lua_State* thread = lua_newthread(L); - int thread_ref = luaL_ref(L, -2); + simple_callback_data* data = prepare_lua_callback(L); + const char* name = luaL_checkstring(L, 2); + pa_cvolume* volume = volume_from_lua(L, 3); - // Copy the callback function to the thread's stack - lua_pushvalue(L, 2); - lua_xmove(L, thread, 1); + pa_operation* op = pa_context_set_sink_volume_by_name(ctx->context, name, volume, success_callback, data); + if (op == NULL) { + int error = pa_context_errno(ctx->context); + lua_pushvalue(L, 2); + lua_pushfstring(L, "failed to set sink volume by name: %s", pa_strerror(error)); + lua_call(L, 1, 0); + } - // List to store the values in - lua_newtable(thread); + // TODO: Is this too early? + // It's probably fine when the operation failed, but in the other case, this might have to be moved to the callback. + // But once this is userdata, I can simply put it onto the callback's thread stack and have Lua garbage collect it. + pa_xfree((void*) volume); + + return 0; +} + + +int context_set_sink_volume_by_index(lua_State* L) { + lua_pa_context* ctx = luaL_checkudata(L, 1, LUA_PA_CONTEXT); + lua_Integer index = luaL_checkinteger(L, 2); + if (index < 1) { + return luaL_error(L, "Sink index out of bounds. Got %d", index); + } + + if (pa_context_get_state(ctx->context) != PA_CONTEXT_READY) { + lua_pushvalue(L, 2); + lua_pushstring(L, "connection not ready"); + lua_call(L, 1, 0); + return 0; + } + + simple_callback_data* data = prepare_lua_callback(L); + pa_cvolume* volume = volume_from_lua(L, 3); + + pa_operation* op = + pa_context_set_sink_volume_by_index(ctx->context, (uint32_t) index - 1, volume, success_callback, data); + if (op == NULL) { + int error = pa_context_errno(ctx->context); + lua_pushvalue(L, 2); + lua_pushfstring(L, "failed to set sink volume by index: %s", pa_strerror(error)); + lua_call(L, 1, 0); + } + + // TODO: Is this too early? + // It's probably fine when the operation failed, but in the other case, this might have to be moved to the callback. + // But once this is userdata, I can simply put it onto the callback's thread stack and have Lua garbage collect it. + pa_xfree((void*) volume); + + return 0; +} + + +int context_set_sink_volume(lua_State* L) { + switch (lua_type(L, 2)) { + case LUA_TSTRING: { + return context_set_sink_volume_by_name(L); + } + case LUA_TNUMBER: { + return context_set_sink_volume_by_index(L); + } + default: { + lua_pushfstring(L, "expected number or string, got %s", luaL_typename(L, 2)); + return luaL_argerror(L, 2, lua_tostring(L, -1)); + } + } +} + + +int context_set_sink_mute_by_name(lua_State* L) { + lua_pa_context* ctx = luaL_checkudata(L, 1, LUA_PA_CONTEXT); + const char* name = luaL_checkstring(L, 2); + int mute = lua_toboolean(L, 3); + + if (pa_context_get_state(ctx->context) != PA_CONTEXT_READY) { + lua_pushvalue(L, 2); + lua_pushstring(L, "connection not ready"); + lua_call(L, 1, 0); + return 0; + } + + simple_callback_data* data = prepare_lua_callback(L); + + pa_operation* op = pa_context_set_sink_mute_by_name(ctx->context, name, mute, success_callback, data); + if (op == NULL) { + int error = pa_context_errno(ctx->context); + lua_pushvalue(L, 2); + lua_pushfstring(L, "failed to set sink mute by name: %s", pa_strerror(error)); + lua_call(L, 1, 0); + return 0; + } + + return 0; +} + + +int context_set_sink_mute_by_index(lua_State* L) { + lua_pa_context* ctx = luaL_checkudata(L, 1, LUA_PA_CONTEXT); + lua_Integer index = luaL_checkinteger(L, 2); + if (index < 1) { + return luaL_error(L, "Sink index out of bounds. Got %d", index); + } + int mute = lua_toboolean(L, 3); + + if (pa_context_get_state(ctx->context) != PA_CONTEXT_READY) { + lua_pushvalue(L, 2); + lua_pushstring(L, "connection not ready"); + lua_call(L, 1, 0); + return 0; + } + + simple_callback_data* data = prepare_lua_callback(L); + + pa_operation* op = + pa_context_set_sink_mute_by_index(ctx->context, (uint32_t) index - 1, mute, success_callback, data); + if (op == NULL) { + int error = pa_context_errno(ctx->context); + lua_pushvalue(L, 2); + lua_pushfstring(L, "failed to set sink mute by index: %s", pa_strerror(error)); + lua_call(L, 1, 0); + return 0; + } + + return 0; +} + + +int context_set_sink_mute(lua_State* L) { + switch (lua_type(L, 2)) { + case LUA_TSTRING: { + return context_set_sink_mute_by_name(L); + } + case LUA_TNUMBER: { + return context_set_sink_mute_by_index(L); + } + default: { + lua_pushfstring(L, "expected number or string, got %s", luaL_typename(L, 2)); + return luaL_argerror(L, 2, lua_tostring(L, -1)); + } + } +} + + +int context_set_sink_suspended_by_name(lua_State* L) { + lua_pa_context* ctx = luaL_checkudata(L, 1, LUA_PA_CONTEXT); + const char* name = luaL_checkstring(L, 2); + int suspended = lua_toboolean(L, 3); + + if (pa_context_get_state(ctx->context) != PA_CONTEXT_READY) { + lua_pushvalue(L, 2); + lua_pushstring(L, "connection not ready"); + lua_call(L, 1, 0); + return 0; + } + + simple_callback_data* data = prepare_lua_callback(L); + + pa_operation* op = pa_context_suspend_sink_by_name(ctx->context, name, suspended, success_callback, data); + if (op == NULL) { + int error = pa_context_errno(ctx->context); + lua_pushvalue(L, 2); + lua_pushfstring(L, "failed to set sink suspended by name: %s", pa_strerror(error)); + lua_call(L, 1, 0); + return 0; + } + + return 0; +} + + +int context_set_sink_suspended_by_index(lua_State* L) { + lua_pa_context* ctx = luaL_checkudata(L, 1, LUA_PA_CONTEXT); + lua_Integer index = luaL_checkinteger(L, 2); + if (index < 1) { + return luaL_error(L, "Sink index out of bounds. Got %d", index); + } + int suspended = lua_toboolean(L, 3); + + if (pa_context_get_state(ctx->context) != PA_CONTEXT_READY) { + lua_pushvalue(L, 2); + lua_pushstring(L, "connection not ready"); + lua_call(L, 1, 0); + return 0; + } + + simple_callback_data* data = prepare_lua_callback(L); + + pa_operation* op = + pa_context_suspend_sink_by_index(ctx->context, (uint32_t) index - 1, suspended, success_callback, data); + if (op == NULL) { + int error = pa_context_errno(ctx->context); + lua_pushvalue(L, 2); + lua_pushfstring(L, "failed to set sink suspended by index: %s", pa_strerror(error)); + lua_call(L, 1, 0); + return 0; + } + + return 0; +} + + +int context_set_sink_suspended(lua_State* L) { + switch (lua_type(L, 2)) { + case LUA_TSTRING: { + return context_set_sink_suspended_by_name(L); + } + case LUA_TNUMBER: { + return context_set_sink_suspended_by_index(L); + } + default: { + lua_pushfstring(L, "expected number or string, got %s", luaL_typename(L, 2)); + return luaL_argerror(L, 2, lua_tostring(L, -1)); + } + } +} + + +// Can be used both functions that produce a single value or the `_list` versions. +// The `is_list` value on the userdata needs to be set accordingly. +// For lists, the callback will be called multiple times, once for each entry. +// The function that sets up the callback must make sure to push a table on the stack, where the +// values can be collected. +// +// @tparam pa_source_info* info Pointer to the data. +// @tparam boolean eol Indicates whether the last item of the list was reached. +// @tparam simple_callback_data* userdata Expected to be an instance of `simple_callback_data`. +void source_info_callback(pa_context* c, const pa_source_info* info, int eol, void* userdata) { + simple_callback_data* data = (simple_callback_data*) userdata; + lua_State* L = data->L; + + if (data->is_list) { + if (!eol) { + int i = lua_objlen(L, 2); + lua_pushinteger(L, i + 1); + source_info_to_lua(L, info); + lua_settable(L, 2); + } else { + // Insert the error argument + lua_pushnil(L); + lua_insert(L, -2); + + lua_call(L, 2, 0); + + free_lua_callback(data); + } + } else { + if (!eol) { + lua_pushfstring(L, "only one source info expected, but got multiple"); + lua_call(L, 1, 0); + } else { + lua_pushnil(L); + source_info_to_lua(L, info); + + lua_call(L, 2, 0); + + free_lua_callback(data); + } + } +} + + +int context_get_source_info_list(lua_State* L) { + lua_pa_context* ctx = luaL_checkudata(L, 1, LUA_PA_CONTEXT); + + if (pa_context_get_state(ctx->context) != PA_CONTEXT_READY) { + lua_pushvalue(L, 2); + lua_pushstring(L, "connection not ready"); + lua_call(L, 1, 0); + return 0; + } + + simple_callback_data* data = prepare_lua_callback(L); + data->is_list = true; + // Create the list to store infos in + lua_newtable(data->L); + + pa_operation* op = pa_context_get_source_info_list(ctx->context, source_info_callback, data); + if (op == NULL) { + int error = pa_context_errno(ctx->context); + lua_pushvalue(L, 2); + lua_pushfstring(L, "failed to get source info list: %s", pa_strerror(error)); + lua_call(L, 1, 0); + return 0; + } + + return 0; +} + + +int context_get_source_info_by_name(lua_State* L) { + lua_pa_context* ctx = luaL_checkudata(L, 1, LUA_PA_CONTEXT); + const char* name = luaL_checkstring(L, 2); + + if (pa_context_get_state(ctx->context) != PA_CONTEXT_READY) { + lua_pushvalue(L, 2); + lua_pushstring(L, "connection not ready"); + lua_call(L, 1, 0); + return 0; + } + + simple_callback_data* data = prepare_lua_callback(L); + + pa_operation* op = pa_context_get_source_info_by_name(ctx->context, name, source_info_callback, data); + if (op == NULL) { + int error = pa_context_errno(ctx->context); + lua_pushvalue(L, 2); + lua_pushfstring(L, "failed to get source info by name: %s", pa_strerror(error)); + lua_call(L, 1, 0); + return 0; + } + + return 0; +} + + +int context_get_source_info_by_index(lua_State* L) { + lua_pa_context* ctx = luaL_checkudata(L, 1, LUA_PA_CONTEXT); + lua_Integer index = luaL_checkinteger(L, 2); + if (index < 1) { + return luaL_error(L, "Source index out of bounds. Got %d", index); + } + + if (pa_context_get_state(ctx->context) != PA_CONTEXT_READY) { + lua_pushvalue(L, 2); + lua_pushstring(L, "connection not ready"); + lua_call(L, 1, 0); + return 0; + } + + simple_callback_data* data = prepare_lua_callback(L); + + pa_operation* op = + pa_context_get_source_info_by_index(ctx->context, (uint32_t) index - 1, source_info_callback, data); + if (op == NULL) { + int error = pa_context_errno(ctx->context); + lua_pushvalue(L, 2); + lua_pushfstring(L, "failed to get source info by index: %s", pa_strerror(error)); + lua_call(L, 1, 0); + return 0; + } + + return 0; +} + + +int context_get_source_info(lua_State* L) { + switch (lua_type(L, 2)) { + case LUA_TSTRING: { + return context_get_source_info_by_name(L); + } + case LUA_TNUMBER: { + return context_get_source_info_by_index(L); + } + default: { + lua_pushfstring(L, "expected number or string, got %s", luaL_typename(L, 2)); + return luaL_argerror(L, 2, lua_tostring(L, -1)); + } + } +} + + +int context_set_source_volume_by_name(lua_State* L) { + lua_pa_context* ctx = luaL_checkudata(L, 1, LUA_PA_CONTEXT); + + if (pa_context_get_state(ctx->context) != PA_CONTEXT_READY) { + lua_pushvalue(L, 2); + lua_pushstring(L, "connection not ready"); + lua_call(L, 1, 0); + return 0; + } + + simple_callback_data* data = prepare_lua_callback(L); + const char* name = luaL_checkstring(L, 2); + pa_cvolume* volume = volume_from_lua(L, 3); + + pa_operation* op = pa_context_set_source_volume_by_name(ctx->context, name, volume, success_callback, data); + if (op == NULL) { + int error = pa_context_errno(ctx->context); + lua_pushvalue(L, 2); + lua_pushfstring(L, "failed to set source volume by name: %s", pa_strerror(error)); + lua_call(L, 1, 0); + } + + // TODO: Is this too early? + // It's probably fine when the operation failed, but in the other case, this might have to be moved to the callback. + // But once this is userdata, I can simply put it onto the callback's thread stack and have Lua garbage collect it. + pa_xfree((void*) volume); + + return 0; +} + + +int context_set_source_volume_by_index(lua_State* L) { + lua_pa_context* ctx = luaL_checkudata(L, 1, LUA_PA_CONTEXT); + lua_Integer index = luaL_checkinteger(L, 2); + if (index < 1) { + return luaL_error(L, "Source index out of bounds. Got %d", index); + } + + if (pa_context_get_state(ctx->context) != PA_CONTEXT_READY) { + lua_pushvalue(L, 2); + lua_pushstring(L, "connection not ready"); + lua_call(L, 1, 0); + return 0; + } + + simple_callback_data* data = prepare_lua_callback(L); + pa_cvolume* volume = volume_from_lua(L, 3); + + pa_operation* op = + pa_context_set_source_volume_by_index(ctx->context, (uint32_t) index - 1, volume, success_callback, data); + if (op == NULL) { + int error = pa_context_errno(ctx->context); + lua_pushvalue(L, 2); + lua_pushfstring(L, "failed to set source volume by index: %s", pa_strerror(error)); + lua_call(L, 1, 0); + } + + // TODO: Is this too early? + // It's probably fine when the operation failed, but in the other case, this might have to be moved to the callback. + // But once this is userdata, I can simply put it onto the callback's thread stack and have Lua garbage collect it. + pa_xfree((void*) volume); + + return 0; +} + + +int context_set_source_volume(lua_State* L) { + switch (lua_type(L, 2)) { + case LUA_TSTRING: { + return context_set_source_volume_by_name(L); + } + case LUA_TNUMBER: { + return context_set_source_volume_by_index(L); + } + default: { + lua_pushfstring(L, "expected number or string, got %s", luaL_typename(L, 2)); + return luaL_argerror(L, 2, lua_tostring(L, -1)); + } + } +} + + +int context_set_source_mute_by_name(lua_State* L) { + lua_pa_context* ctx = luaL_checkudata(L, 1, LUA_PA_CONTEXT); + const char* name = luaL_checkstring(L, 2); + int mute = lua_toboolean(L, 3); + + if (pa_context_get_state(ctx->context) != PA_CONTEXT_READY) { + lua_pushvalue(L, 2); + lua_pushstring(L, "connection not ready"); + lua_call(L, 1, 0); + return 0; + } + + simple_callback_data* data = prepare_lua_callback(L); + + pa_operation* op = pa_context_set_source_mute_by_name(ctx->context, name, mute, success_callback, data); + if (op == NULL) { + int error = pa_context_errno(ctx->context); + lua_pushvalue(L, 2); + lua_pushfstring(L, "failed to set source mute by name: %s", pa_strerror(error)); + lua_call(L, 1, 0); + return 0; + } + + return 0; +} + + +int context_set_source_mute_by_index(lua_State* L) { + lua_pa_context* ctx = luaL_checkudata(L, 1, LUA_PA_CONTEXT); + lua_Integer index = luaL_checkinteger(L, 2); + if (index < 1) { + return luaL_error(L, "Source index out of bounds. Got %d", index); + } + int mute = lua_toboolean(L, 3); + + if (pa_context_get_state(ctx->context) != PA_CONTEXT_READY) { + lua_pushvalue(L, 2); + lua_pushstring(L, "connection not ready"); + lua_call(L, 1, 0); + return 0; + } + + simple_callback_data* data = prepare_lua_callback(L); + + pa_operation* op = + pa_context_set_source_mute_by_index(ctx->context, (uint32_t) index - 1, mute, success_callback, data); + if (op == NULL) { + int error = pa_context_errno(ctx->context); + lua_pushvalue(L, 2); + lua_pushfstring(L, "failed to set source mute by index: %s", pa_strerror(error)); + lua_call(L, 1, 0); + return 0; + } + + return 0; +} + + +int context_set_source_mute(lua_State* L) { + switch (lua_type(L, 2)) { + case LUA_TSTRING: { + return context_set_source_mute_by_name(L); + } + case LUA_TNUMBER: { + return context_set_source_mute_by_index(L); + } + default: { + lua_pushfstring(L, "expected number or string, got %s", luaL_typename(L, 2)); + return luaL_argerror(L, 2, lua_tostring(L, -1)); + } + } +} + + +int context_set_source_suspended_by_name(lua_State* L) { + lua_pa_context* ctx = luaL_checkudata(L, 1, LUA_PA_CONTEXT); + const char* name = luaL_checkstring(L, 2); + int suspended = lua_toboolean(L, 3); + + if (pa_context_get_state(ctx->context) != PA_CONTEXT_READY) { + lua_pushvalue(L, 2); + lua_pushstring(L, "connection not ready"); + lua_call(L, 1, 0); + return 0; + } + + simple_callback_data* data = prepare_lua_callback(L); + + pa_operation* op = pa_context_suspend_source_by_name(ctx->context, name, suspended, success_callback, data); + if (op == NULL) { + int error = pa_context_errno(ctx->context); + lua_pushvalue(L, 2); + lua_pushfstring(L, "failed to set source suspended by name: %s", pa_strerror(error)); + lua_call(L, 1, 0); + return 0; + } + + return 0; +} + + +int context_set_source_suspended_by_index(lua_State* L) { + lua_pa_context* ctx = luaL_checkudata(L, 1, LUA_PA_CONTEXT); + lua_Integer index = luaL_checkinteger(L, 2); + if (index < 1) { + return luaL_error(L, "Source index out of bounds. Got %d", index); + } + int suspended = lua_toboolean(L, 3); + + if (pa_context_get_state(ctx->context) != PA_CONTEXT_READY) { + lua_pushvalue(L, 2); + lua_pushstring(L, "connection not ready"); + lua_call(L, 1, 0); + return 0; + } + + simple_callback_data* data = prepare_lua_callback(L); + + pa_operation* op = + pa_context_suspend_source_by_index(ctx->context, (uint32_t) index - 1, suspended, success_callback, data); + if (op == NULL) { + int error = pa_context_errno(ctx->context); + lua_pushvalue(L, 2); + lua_pushfstring(L, "failed to set source suspended by index: %s", pa_strerror(error)); + lua_call(L, 1, 0); + return 0; + } + + return 0; +} + + +int context_set_source_suspended(lua_State* L) { + switch (lua_type(L, 2)) { + case LUA_TSTRING: { + return context_set_source_suspended_by_name(L); + } + case LUA_TNUMBER: { + return context_set_source_suspended_by_index(L); + } + default: { + lua_pushfstring(L, "expected number or string, got %s", luaL_typename(L, 2)); + return luaL_argerror(L, 2, lua_tostring(L, -1)); + } + } +} + + +void sink_input_info_callback(pa_context* c, const pa_sink_input_info* info, int eol, void* userdata) { + simple_callback_data* data = (simple_callback_data*) userdata; + lua_State* L = data->L; + + if (data->is_list) { + if (!eol) { + int i = lua_objlen(L, 2); + lua_pushinteger(L, i + 1); + sink_input_info_to_lua(L, info); + lua_settable(L, 2); + } else { + // Insert the error argument + lua_pushnil(L); + lua_insert(L, -2); + + lua_call(L, 2, 0); + + free_lua_callback(data); + } + } else { + if (!eol) { + lua_pushfstring(L, "only one sink input info expected, but got multiple"); + lua_call(L, 1, 0); + } else { + lua_pushnil(L); + sink_input_info_to_lua(L, info); + + lua_call(L, 2, 0); + + free_lua_callback(data); + } + } +} + + +int context_get_sink_input_info_list(lua_State* L) { + lua_pa_context* ctx = luaL_checkudata(L, 1, LUA_PA_CONTEXT); + + if (pa_context_get_state(ctx->context) != PA_CONTEXT_READY) { + lua_pushvalue(L, 2); + lua_pushstring(L, "connection not ready"); + lua_call(L, 1, 0); + return 0; + } + + simple_callback_data* data = prepare_lua_callback(L); + data->is_list = true; + // Create the list to store infos in + lua_newtable(data->L); + + pa_operation* op = pa_context_get_sink_input_info_list(ctx->context, sink_input_info_callback, data); + if (op == NULL) { + int error = pa_context_errno(ctx->context); + lua_pushvalue(L, 2); + lua_pushfstring(L, "failed to get source info list: %s", pa_strerror(error)); + lua_call(L, 1, 0); + return 0; + } + + return 0; +} + + +int context_get_sink_input_info(lua_State* L) { + lua_pa_context* ctx = luaL_checkudata(L, 1, LUA_PA_CONTEXT); + lua_Integer index = luaL_checkinteger(L, 2); + if (index < 1) { + return luaL_error(L, "Sink input index out of bounds. Got %d", index); + } + + if (pa_context_get_state(ctx->context) != PA_CONTEXT_READY) { + lua_pushvalue(L, 2); + lua_pushstring(L, "connection not ready"); + lua_call(L, 1, 0); + return 0; + } + + simple_callback_data* data = prepare_lua_callback(L); + + pa_operation* op = + pa_context_get_sink_input_info(ctx->context, (uint32_t) index - 1, sink_input_info_callback, data); + if (op == NULL) { + int error = pa_context_errno(ctx->context); + lua_pushvalue(L, 2); + lua_pushfstring(L, "failed to get sink input info by index: %s", pa_strerror(error)); + lua_call(L, 1, 0); + return 0; + } + + return 0; +} + + +int context_move_sink_input(lua_State* L) { + switch (lua_type(L, 2)) { + case LUA_TSTRING: { + return context_move_sink_input_by_name(L); + } + case LUA_TNUMBER: { + return context_move_sink_input_by_index(L); + } + default: { + lua_pushfstring(L, "expected number or string, got %s", luaL_typename(L, 2)); + return luaL_argerror(L, 2, lua_tostring(L, -1)); + } + } +} + + +int context_move_sink_input_by_index(lua_State* L) { + lua_pa_context* ctx = luaL_checkudata(L, 1, LUA_PA_CONTEXT); + lua_Integer sink_input_index = luaL_checkinteger(L, 2); + if (sink_input_index < 1) { + return luaL_error(L, "Sink input index out of bounds. Got %d", sink_input_index); + } + + lua_Integer sink_index = luaL_checkinteger(L, 3); + if (sink_index < 1) { + return luaL_error(L, "Sink index out of bounds. Got %d", sink_index); + } + + if (pa_context_get_state(ctx->context) != PA_CONTEXT_READY) { + lua_pushvalue(L, 2); + lua_pushstring(L, "connection not ready"); + lua_call(L, 1, 0); + return 0; + } + + simple_callback_data* data = prepare_lua_callback(L); + + pa_operation* op = pa_context_move_sink_input_by_index(ctx->context, + (uint32_t) sink_input_index - 1, + (uint32_t) sink_index - 1, + success_callback, + data); + if (op == NULL) { + int error = pa_context_errno(ctx->context); + lua_pushvalue(L, 2); + lua_pushfstring(L, + "failed to move sink input %d to sink %d: %s", + sink_input_index, + sink_index, + pa_strerror(error)); + lua_call(L, 1, 0); + return 0; + } + + return 0; +} + + +int context_move_sink_input_by_name(lua_State* L) { + lua_pa_context* ctx = luaL_checkudata(L, 1, LUA_PA_CONTEXT); + lua_Integer sink_input_index = luaL_checkinteger(L, 2); + if (sink_input_index < 1) { + return luaL_error(L, "Sink input index out of bounds. Got %d", sink_input_index); + } + + const char* sink_name = luaL_checkstring(L, 3); + + if (pa_context_get_state(ctx->context) != PA_CONTEXT_READY) { + lua_pushvalue(L, 2); + lua_pushstring(L, "connection not ready"); + lua_call(L, 1, 0); + return 0; + } + + simple_callback_data* data = prepare_lua_callback(L); + + pa_operation* op = pa_context_move_sink_input_by_name(ctx->context, + (uint32_t) sink_input_index - 1, + sink_name, + success_callback, + data); + if (op == NULL) { + int error = pa_context_errno(ctx->context); + lua_pushvalue(L, 2); + lua_pushfstring(L, + "failed to move sink input %d to sink %s: %s", + sink_input_index, + sink_name, + pa_strerror(error)); + lua_call(L, 1, 0); + return 0; + } + + return 0; +} + + +int context_set_sink_input_volume(lua_State* L) { + lua_pa_context* ctx = luaL_checkudata(L, 1, LUA_PA_CONTEXT); + lua_Integer index = luaL_checkinteger(L, 2); + if (index < 1) { + return luaL_error(L, "Sink input index out of bounds. Got %d", index); + } + pa_cvolume* volume = volume_from_lua(L, 3); + + simple_callback_data* data = prepare_lua_callback(L); + + pa_operation* op = + pa_context_set_sink_input_volume(ctx->context, (uint32_t) index - 1, volume, success_callback, data); + if (op == NULL) { + int error = pa_context_errno(ctx->context); + lua_pushvalue(L, 2); + lua_pushfstring(L, "failed to set volume for sink input %d: %s", index, pa_strerror(error)); + lua_call(L, 1, 0); + return 0; + } + + return 0; +} + + +int context_set_sink_input_mute(lua_State* L) { + lua_pa_context* ctx = luaL_checkudata(L, 1, LUA_PA_CONTEXT); + lua_Integer index = luaL_checkinteger(L, 2); + if (index < 1) { + return luaL_error(L, "Sink input index out of bounds. Got %d", index); + } + bool mute = lua_toboolean(L, 3); + + simple_callback_data* data = prepare_lua_callback(L); + + pa_operation* op = pa_context_set_sink_input_mute(ctx->context, (uint32_t) index - 1, mute, success_callback, data); + if (op == NULL) { + int error = pa_context_errno(ctx->context); + lua_pushvalue(L, 2); + lua_pushfstring(L, "failed to set mute for sink input %d: %s", index, pa_strerror(error)); + lua_call(L, 1, 0); + return 0; + } + + return 0; +} + + +int context_kill_sink_input(lua_State* L) { + lua_pa_context* ctx = luaL_checkudata(L, 1, LUA_PA_CONTEXT); + lua_Integer index = luaL_checkinteger(L, 2); + if (index < 1) { + return luaL_error(L, "Sink input index out of bounds. Got %d", index); + } + + + simple_callback_data* data = prepare_lua_callback(L); + + pa_operation* op = pa_context_kill_sink_input(ctx->context, (uint32_t) index - 1, success_callback, data); + if (op == NULL) { + int error = pa_context_errno(ctx->context); + lua_pushvalue(L, 2); + lua_pushfstring(L, "failed to kill sink input %d: %s", index, pa_strerror(error)); + lua_call(L, 1, 0); + return 0; + } + + return 0; +} + + +void source_output_info_callback(pa_context* c, const pa_source_output_info* info, int eol, void* userdata) { + simple_callback_data* data = (simple_callback_data*) userdata; + lua_State* L = data->L; + + if (data->is_list) { + if (!eol) { + int i = lua_objlen(L, 2); + lua_pushinteger(L, i + 1); + source_output_info_to_lua(L, info); + lua_settable(L, 2); + } else { + // Insert the error argument + lua_pushnil(L); + lua_insert(L, -2); + + lua_call(L, 2, 0); + + free_lua_callback(data); + } + } else { + if (!eol) { + lua_pushfstring(L, "only one source output info expected, but got multiple"); + lua_call(L, 1, 0); + } else { + lua_pushnil(L); + source_output_info_to_lua(L, info); + + lua_call(L, 2, 0); + + free_lua_callback(data); + } + } +} + + +int context_get_source_output_info_list(lua_State* L) { + lua_pa_context* ctx = luaL_checkudata(L, 1, LUA_PA_CONTEXT); + + if (pa_context_get_state(ctx->context) != PA_CONTEXT_READY) { + lua_pushvalue(L, 2); + lua_pushstring(L, "connection not ready"); + lua_call(L, 1, 0); + return 0; + } + + simple_callback_data* data = prepare_lua_callback(L); + data->is_list = true; + // Create the list to store infos in + lua_newtable(data->L); + + pa_operation* op = pa_context_get_source_output_info_list(ctx->context, source_output_info_callback, data); + if (op == NULL) { + int error = pa_context_errno(ctx->context); + lua_pushvalue(L, 2); + lua_pushfstring(L, "failed to get source info list: %s", pa_strerror(error)); + lua_call(L, 1, 0); + return 0; + } + + return 0; +} + + +int context_get_source_output_info(lua_State* L) { + lua_pa_context* ctx = luaL_checkudata(L, 1, LUA_PA_CONTEXT); + lua_Integer index = luaL_checkinteger(L, 2); + if (index < 1) { + return luaL_error(L, "Source output index out of bounds. Got %d", index); + } + + if (pa_context_get_state(ctx->context) != PA_CONTEXT_READY) { + lua_pushvalue(L, 2); + lua_pushstring(L, "connection not ready"); + lua_call(L, 1, 0); + return 0; + } + + simple_callback_data* data = prepare_lua_callback(L); + + pa_operation* op = + pa_context_get_source_output_info(ctx->context, (uint32_t) index - 1, source_output_info_callback, data); + if (op == NULL) { + int error = pa_context_errno(ctx->context); + lua_pushvalue(L, 2); + lua_pushfstring(L, "failed to get source output info by index: %s", pa_strerror(error)); + lua_call(L, 1, 0); + return 0; + } + + return 0; +} + + +int context_move_source_output(lua_State* L) { + switch (lua_type(L, 2)) { + case LUA_TSTRING: { + return context_move_source_output_by_name(L); + } + case LUA_TNUMBER: { + return context_move_source_output_by_index(L); + } + default: { + lua_pushfstring(L, "expected number or string, got %s", luaL_typename(L, 2)); + return luaL_argerror(L, 2, lua_tostring(L, -1)); + } + } +} + + +int context_move_source_output_by_index(lua_State* L) { + lua_pa_context* ctx = luaL_checkudata(L, 1, LUA_PA_CONTEXT); + lua_Integer source_output_index = luaL_checkinteger(L, 2); + if (source_output_index < 1) { + return luaL_error(L, "Source output index out of bounds. Got %d", source_output_index); + } + + lua_Integer source_index = luaL_checkinteger(L, 3); + if (source_index < 1) { + return luaL_error(L, "Source index out of bounds. Got %d", source_index); + } + + if (pa_context_get_state(ctx->context) != PA_CONTEXT_READY) { + lua_pushvalue(L, 2); + lua_pushstring(L, "connection not ready"); + lua_call(L, 1, 0); + return 0; + } + + simple_callback_data* data = prepare_lua_callback(L); + + pa_operation* op = pa_context_move_source_output_by_index(ctx->context, + (uint32_t) source_output_index - 1, + (uint32_t) source_index - 1, + success_callback, + data); + if (op == NULL) { + int error = pa_context_errno(ctx->context); + lua_pushvalue(L, 2); + lua_pushfstring(L, + "failed to move source output %d to source %d: %s", + source_output_index, + source_index, + pa_strerror(error)); + lua_call(L, 1, 0); + return 0; + } + + return 0; +} + + +int context_move_source_output_by_name(lua_State* L) { + lua_pa_context* ctx = luaL_checkudata(L, 1, LUA_PA_CONTEXT); + lua_Integer source_output_index = luaL_checkinteger(L, 2); + if (source_output_index < 1) { + return luaL_error(L, "Source output index out of bounds. Got %d", source_output_index); + } + + const char* source_name = luaL_checkstring(L, 3); + + if (pa_context_get_state(ctx->context) != PA_CONTEXT_READY) { + lua_pushvalue(L, 2); + lua_pushstring(L, "connection not ready"); + lua_call(L, 1, 0); + return 0; + } + + simple_callback_data* data = prepare_lua_callback(L); + + pa_operation* op = pa_context_move_source_output_by_name(ctx->context, + (uint32_t) source_output_index - 1, + source_name, + success_callback, + data); + if (op == NULL) { + int error = pa_context_errno(ctx->context); + lua_pushvalue(L, 2); + lua_pushfstring(L, + "failed to move source output %d to source %s: %s", + source_output_index, + source_name, + pa_strerror(error)); + lua_call(L, 1, 0); + return 0; + } + + return 0; +} + + +int context_set_source_output_volume(lua_State* L) { + lua_pa_context* ctx = luaL_checkudata(L, 1, LUA_PA_CONTEXT); + lua_Integer index = luaL_checkinteger(L, 2); + if (index < 1) { + return luaL_error(L, "Source output index out of bounds. Got %d", index); + } + pa_cvolume* volume = volume_from_lua(L, 3); + + simple_callback_data* data = prepare_lua_callback(L); + + pa_operation* op = + pa_context_set_source_output_volume(ctx->context, (uint32_t) index - 1, volume, success_callback, data); + if (op == NULL) { + int error = pa_context_errno(ctx->context); + lua_pushvalue(L, 2); + lua_pushfstring(L, "failed to set volume for source output %d: %s", index, pa_strerror(error)); + lua_call(L, 1, 0); + return 0; + } + + return 0; +} + + +int context_set_source_output_mute(lua_State* L) { + lua_pa_context* ctx = luaL_checkudata(L, 1, LUA_PA_CONTEXT); + lua_Integer index = luaL_checkinteger(L, 2); + if (index < 1) { + return luaL_error(L, "Source output index out of bounds. Got %d", index); + } + bool mute = lua_toboolean(L, 3); + + simple_callback_data* data = prepare_lua_callback(L); + + pa_operation* op = + pa_context_set_source_output_mute(ctx->context, (uint32_t) index - 1, mute, success_callback, data); + if (op == NULL) { + int error = pa_context_errno(ctx->context); + lua_pushvalue(L, 2); + lua_pushfstring(L, "failed to set mute for source output %d: %s", index, pa_strerror(error)); + lua_call(L, 1, 0); + return 0; + } + + return 0; +} + + +int context_kill_source_output(lua_State* L) { + lua_pa_context* ctx = luaL_checkudata(L, 1, LUA_PA_CONTEXT); + lua_Integer index = luaL_checkinteger(L, 2); + if (index < 1) { + return luaL_error(L, "Source output index out of bounds. Got %d", index); + } + + + simple_callback_data* data = prepare_lua_callback(L); + + pa_operation* op = pa_context_kill_source_output(ctx->context, (uint32_t) index - 1, success_callback, data); + if (op == NULL) { + int error = pa_context_errno(ctx->context); + lua_pushvalue(L, 2); + lua_pushfstring(L, "failed to kill source output %d: %s", index, pa_strerror(error)); + lua_call(L, 1, 0); + return 0; + } - source_info_list_callback_data* data = calloc(1, sizeof(struct source_info_list_callback_data)); - data->L = thread; - data->thread_ref = thread_ref; - - pa_context_get_source_info_list(ctx->context, source_info_list_callback, data); return 0; } diff --git a/src/lua_libpulse_glib/introspection.h b/src/lua_libpulse_glib/introspection.h deleted file mode 100644 index c998d16..0000000 --- a/src/lua_libpulse_glib/introspection.h +++ /dev/null @@ -1,10 +0,0 @@ -#pragma once - -#include "lua.h" - -int -context_get_server_info(lua_State*); -int -context_get_sink_info_list(lua_State*); -int -context_get_source_info_list(lua_State*); diff --git a/src/lua_libpulse_glib/lua_util.h b/src/lua_libpulse_glib/lua_util.h new file mode 100644 index 0000000..9f7a903 --- /dev/null +++ b/src/lua_libpulse_glib/lua_util.h @@ -0,0 +1,15 @@ +#ifndef lua_util_h_INCLUDED +#define lua_util_h_INCLUDED + +#include +#include + +#define LUA_MOD_EXPORT extern + + +typedef struct luaU_enumfield { + const char* name; + const char* value; +} luaU_enumfield; + +#endif // lua_util_h_INCLUDED diff --git a/src/lua_libpulse_glib/proplist.c b/src/lua_libpulse_glib/proplist.c new file mode 100644 index 0000000..35c6a91 --- /dev/null +++ b/src/lua_libpulse_glib/proplist.c @@ -0,0 +1,214 @@ +#include "proplist.h" + +#include + +// Creates a Lua userdatum from/for a PulseAudio proplist. +// +// The provided proplist will be `pa_proplist_copy()`ed to pass memory management over +// to the garbage collector. +// +// @return[type=PropList] +int proplist_to_lua(lua_State* L, pa_proplist* pa_plist) { + proplist* plist = lua_newuserdata(L, sizeof(proplist)); + if (pa_plist == NULL) { + lua_pushfstring(L, "Failed to allocate proplist userdata"); + return lua_error(L); + } + luaL_getmetatable(L, LUA_PA_PROPLIST); + lua_setmetatable(L, -2); + + plist->plist = pa_proplist_copy(pa_plist); + + return 1; +} + + +int proplist_new(lua_State* L) { + pa_proplist* pa_plist = pa_proplist_new(); + if (pa_plist == NULL) { + lua_pushfstring(L, "Failed to allocate PulseAudio proplist"); + return lua_error(L); + } + + return proplist_to_lua(L, pa_plist); +} + + +int proplist_from_string(lua_State* L) { + const char* str = luaL_checkstring(L, 1); + pa_proplist* pa_plist = pa_proplist_from_string(str); + + return proplist_to_lua(L, pa_plist); +} + + +int proplist_key_valid(lua_State* L) { + const char* key = luaL_checkstring(L, 1); + int valid = pa_proplist_key_valid(key); + lua_pushboolean(L, valid); + return 1; +} + + +int proplist_isempty(lua_State* L) { + proplist* plist = luaL_checkudata(L, 1, LUA_PA_PROPLIST); + int empty = pa_proplist_isempty(plist->plist); + lua_pushboolean(L, empty); + return 1; +} + + +int proplist_tostring_sep(lua_State* L) { + proplist* plist = luaL_checkudata(L, 1, LUA_PA_PROPLIST); + const char* sep = luaL_checkstring(L, 2); + const char* str = pa_proplist_to_string_sep(plist->plist, sep); + lua_pushstring(L, str); + pa_xfree((void*) str); + return 1; +} + + +int proplist_clear(lua_State* L) { + proplist* plist = luaL_checkudata(L, 1, LUA_PA_PROPLIST); + pa_proplist_clear(plist->plist); + return 0; +} + + +int proplist_contains(lua_State* L) { + proplist* plist = luaL_checkudata(L, 1, LUA_PA_PROPLIST); + const char* key = luaL_checkstring(L, 2); + if (!pa_proplist_key_valid(key)) { + return luaL_error(L, "invalid key for proplist"); + } + + int contains = pa_proplist_contains(plist->plist, key); + if (contains < 0) { + return luaL_error(L, "failed to check if proplist contains the key"); + } else { + lua_pushboolean(L, contains); + } + + return 1; +} + + +int proplist_copy(lua_State* L) { + proplist* plist = luaL_checkudata(L, 1, LUA_PA_PROPLIST); + pa_proplist* other = pa_proplist_copy(plist->plist); + + return proplist_to_lua(L, other); +} + + +// Metatable functions + + +// Accesses the value at the given string key. +// +// This functions very similar to a regular hash table. +// +// Numeric indices/keys are not supported. +// +// Will return `nil` if there is no value or if it is not valid UTF-8. +// Arbitrary data from `pa_proplist_get()` is currently not supported. +// +// @param[type=string] key The index to access. +// @return[type=string|nil] The data at the given index. +int proplist__index(lua_State* L) { + proplist* plist = luaL_checkudata(L, 1, LUA_PA_PROPLIST); + const char* key = luaL_checkstring(L, 2); + if (!pa_proplist_key_valid(key)) { + return luaL_error(L, "invalid key for proplist"); + } + + if (!pa_proplist_contains(plist->plist, key)) { + lua_pushnil(L); + return 1; + } + + const char* value = pa_proplist_gets(plist->plist, key); + if (value == NULL) { + lua_pushnil(L); + } else { + lua_pushstring(L, value); + } + + return 1; +} + + +// Sets or overwrites the value at the given key. +// +// Passing `nil` as value will unset the key. +// +// For both keys and values only strings are supported. +// Numeric indices or binary data values (as in `pa_proplist_set()`) are not available. +// +// @param[type=string] key The index to write to. +// @param[type=string|nil] value The value to write. +int proplist__newindex(lua_State* L) { + proplist* plist = luaL_checkudata(L, 1, LUA_PA_PROPLIST); + const char* key = luaL_checkstring(L, 2); + + if (lua_isnil(L, 3)) { + if (pa_proplist_unset(plist->plist, key) < 0) { + // TODO: Get last error. Need to get access to the current context for + // this. + return luaL_error(L, "failed to unset key %s", key); + } + } else { + const char* value = luaL_checkstring(L, 3); + if (pa_proplist_sets(plist->plist, key, value) < 0) { + return luaL_error(L, "failed to set value for key %s", key); + } + } + + return 0; +} + + +// Frees the internal proplist. +int proplist__gc(lua_State* L) { + proplist* plist = luaL_checkudata(L, 1, LUA_PA_PROPLIST); + pa_proplist_free(plist->plist); + return 0; +} + + +// Gets the size of the proplist. +int proplist__len(lua_State* L) { + proplist* plist = luaL_checkudata(L, 1, LUA_PA_PROPLIST); + unsigned len = pa_proplist_size(plist->plist); + lua_pushinteger(L, len); + return 1; +} + + +// Compares two proplists for equality. +// +// Proplists are equal if they contain the same keys with the same values. +// +// @param[type=PropList] other The proplist to compare against. +// @return[type=boolean] +int proplist__eq(lua_State* L) { + proplist* plist = luaL_checkudata(L, 1, LUA_PA_PROPLIST); + proplist* other = luaL_checkudata(L, 2, LUA_PA_PROPLIST); + int equal = pa_proplist_equal(plist->plist, other->plist); + lua_pushboolean(L, equal); + return 1; +} + + +// Creates a string representation. +// +// This adds a newline character as separator and as final character. +// +// @return[type=string] +int proplist__tostring(lua_State* L) { + proplist* plist = luaL_checkudata(L, 1, LUA_PA_PROPLIST); + char* str = pa_proplist_to_string(plist->plist); + lua_pushstring(L, str); + pa_xfree((void*) str); + return 1; +} diff --git a/src/lua_libpulse_glib/proplist.h b/src/lua_libpulse_glib/proplist.h new file mode 100644 index 0000000..bacb3ed --- /dev/null +++ b/src/lua_libpulse_glib/proplist.h @@ -0,0 +1,316 @@ +/** Bindings for PulseAudio's property lists. + * + * Basic operations are mapped to Lua table operations: + * + * - get a value: `plist[key]` + * - set a value: `plist[key] = value` + * - get the size: `#plist` + * - get default string presentation: `tostring(plist)` + * - equality: `plist == other` + * + * Additional operations are exposed as methods, as documented below. + * + * @module pulseaudio.proplist + */ +#pragma once + +#include "lua_util.h" + +#include +#include +#include +#include + +#define LUA_PA_PROPLIST "pulseaudio.proplist" + + +typedef struct proplist { + pa_proplist* plist; +} proplist; + + +/** A list of well-known keys. These will likely be used by other consumers + * of PulseAudio, so should be preferred for interoperatibility. + * + * See [proplist.h](https://freedesktop.org/software/pulseaudio/doxygen/proplist_8h.html) for details on + * these values. + * + * local proplist = require("pulseaudio.proplist") + * proplist.is_key_valid(proplist.MEDIA_NAME) -- true + * print(proplist.MEDIA_NAME) -- media.name + * + * @table pulseaudio.proplist + * @field MEDIA_NAME media.name + * @field MEDIA_TITLE media.title + * @field MEDIA_ARTIST media.artist + * @field MEDIA_COPYRIGHT media.copyright + * @field MEDIA_SOFTWARE media.software + * @field MEDIA_LANGUAGE media.language + * @field MEDIA_FILENAME media.filename + * @field MEDIA_ICON media.icon + * @field MEDIA_ICON_NAME media.icon_name + * @field MEDIA_ROLE media.role + * @field FILTER_WANT filter.want + * @field FILTER_APPLY filter.apply + * @field FILTER_SUPPRESS filter.suppress + * @field EVENT_ID event.id + * @field EVENT_DESCRIPTION event.description + * @field EVENT_MOUSE_X event.mouse.x + * @field EVENT_MOUSE_Y event.mouse.y + * @field EVENT_MOUSE_HPOS event.mouse.hpos + * @field EVENT_MOUSE_VPOS event.mouse.vpos + * @field EVENT_MOUSE_BUTTON event.mouse.button + * @field WINDOW_NAME window.name + * @field WINDOW_ID window.id + * @field WINDOW_ICON window.icon + * @field WINDOW_ICON_NAME window.icon_name + * @field WINDOW_X window.x + * @field WINDOW_Y window.y + * @field WINDOW_WIDTH window.width + * @field WINDOW_HEIGHT window.height + * @field WINDOW_HPOS window.hpos + * @field WINDOW_VPOS window.vpos + * @field WINDOW_DESKTOP window.desktop + * @field WINDOW_X11_DISPLAY window.x11.display + * @field WINDOW_X11_SCREEN window.x11.screen + * @field WINDOW_X11_MONITOR window.x11.monitor + * @field WINDOW_X11_XID window.x11.xid + * @field APPLICATION_NAME application.name + * @field APPLICATION_ID application.id + * @field APPLICATION_VERSION application.version + * @field APPLICATION_ICON application.icon + * @field APPLICATION_ICON_NAME application.icon_name + * @field APPLICATION_LANGUAGE application.language + * @field APPLICATION_PROCESS_ID application.process.id + * @field APPLICATION_PROCESS_BINARY application.process.binary + * @field APPLICATION_PROCESS_USER application.process.user + * @field APPLICATION_PROCESS_HOST application.process.host + * @field APPLICATION_PROCESS_MACHINE_ID application.process.machine_id + * @field APPLICATION_PROCESS_SESSION_ID application.process.session_id + * @field DEVICE_STRING device.string + * @field DEVICE_API device.api + * @field DEVICE_DESCRIPTION device.description + * @field DEVICE_BUS_PATH device.bus_path + * @field DEVICE_SERIAL device.serial + * @field DEVICE_VENDOR_ID device.vendor.id + * @field DEVICE_VENDOR_NAME device.vendor.name + * @field DEVICE_PRODUCT_ID device.product.id + * @field DEVICE_PRODUCT_NAME device.product.name + * @field DEVICE_CLASS device.class + * @field DEVICE_FORM_FACTOR device.form_factor + * @field DEVICE_BUS device.bus + * @field DEVICE_ICON device.icon + * @field DEVICE_ICON_NAME device.icon_name + * @field DEVICE_ACCESS_MODE device.access_mode + * @field DEVICE_MASTER_DEVICE device.master_device + * @field DEVICE_BUFFERING_BUFFER_SIZE device.buffering.buffer_size + * @field DEVICE_BUFFERING_FRAGMENT_SIZE device.buffering.fragment_size + * @field DEVICE_PROFILE_NAME device.profile.name + * @field DEVICE_INTENDED_ROLES device.intended_roles + * @field DEVICE_PROFILE_DESCRIPTION device.profile.description + * @field MODULE_AUTHOR module.author + * @field MODULE_DESCRIPTION module.description + * @field MODULE_USAGE module.usage + * @field MODULE_VERSION module.version + * @field FORMAT_SAMPLE_FORMAT format.sample_format + * @field FORMAT_RATE format.rate + * @field FORMAT_CHANNELS format.channels + * @field FORMAT_CHANNEL_MAP format.channel_map + * @field CONTEXT_FORCE_DISABLE_SHM context.force.disable.shm + * @field BLUETOOTH_CODEC bluetooth.codec + */ + + +/// Constructor functions. +/// @section constructors + + +/** Creates a new, empty property list. + * + * @function new + * @return[type=PropList] + */ +int proplist_new(lua_State*); + +/** Parses a string into a @{PropList}. + * + * @function from_string + * @param[type=string] str The string to parse. + * @return[type=PropList] + */ +int proplist_from_string(lua_State*); + + +/// Static functions +/// @section static + + +/** Checks if the given string is a valid key. + * + * @function is_key_valid + * @param[type=string] key The string to check. + * @return[type=boolean] + */ +int proplist_key_valid(lua_State*); + + +// Internal functions + +int proplist_to_lua(lua_State*, pa_proplist*); +int proplist__index(lua_State*); +int proplist__newindex(lua_State*); +int proplist__gc(lua_State*); +int proplist__len(lua_State*); +int proplist__eq(lua_State*); +int proplist__tostring(lua_State*); + + +/// Methods +/// @type PropList + + +/** Checks if the proplist is empty. + * + * @function is_empty + * @return[type=boolean] + */ +int proplist_isempty(lua_State*); + +/** Creates a string representation with a custom separator. + * + * @function tostring_sep + * @return[type=string] + */ +int proplist_tostring_sep(lua_State*); + +/** Removes all keys from the proplist + * + * @function clear + */ +int proplist_clear(lua_State*); + +/** Checks if the proplist contains the given key. + * + * @function contains + * @param[type=string] key The key to check for. + * @return[type=boolean] + */ +int proplist_contains(lua_State*); + +/** Duplicates the proplist. + * + * @function copy + * @return[type=proplist] + */ +int proplist_copy(lua_State*); + + +static const struct luaL_Reg proplist_f[] = { + {"clear", proplist_clear }, + { "contains", proplist_contains }, + { "copy", proplist_copy }, + { "is_empty", proplist_isempty }, + { "tostring_sep", proplist_tostring_sep}, + { NULL, NULL } +}; + + +static const struct luaL_Reg proplist_mt[] = { + {"__gc", proplist__gc }, + { "__len", proplist__len }, + { "__tostring", proplist__tostring}, + { "__eq", proplist__eq }, + { "__index", proplist__index }, + { "__newindex", proplist__newindex}, + { NULL, NULL } +}; + + +static const struct luaL_Reg proplist_lib[] = { + {"new", proplist_new }, + { "is_key_valid", proplist_key_valid }, + { "from_string", proplist_from_string}, + { NULL, NULL } +}; + + +static const struct luaU_enumfield proplist_enum[] = { + {"MEDIA_TITLE", PA_PROP_MEDIA_TITLE }, + { "MEDIA_ARTIST", PA_PROP_MEDIA_ARTIST }, + { "MEDIA_COPYRIGHT", PA_PROP_MEDIA_COPYRIGHT }, + { "MEDIA_SOFTWARE", PA_PROP_MEDIA_SOFTWARE }, + { "MEDIA_LANGUAGE", PA_PROP_MEDIA_LANGUAGE }, + { "MEDIA_FILENAME", PA_PROP_MEDIA_FILENAME }, + { "MEDIA_ICON", PA_PROP_MEDIA_ICON }, + { "MEDIA_ICON_NAME", PA_PROP_MEDIA_ICON_NAME }, + { "MEDIA_ROLE", PA_PROP_MEDIA_ROLE }, + { "FILTER_WANT", PA_PROP_FILTER_WANT }, + { "FILTER_APPLY", PA_PROP_FILTER_APPLY }, + { "FILTER_SUPPRESS", PA_PROP_FILTER_SUPPRESS }, + { "EVENT_ID", PA_PROP_EVENT_ID }, + { "EVENT_DESCRIPTION", PA_PROP_EVENT_DESCRIPTION }, + { "EVENT_MOUSE_X", PA_PROP_EVENT_MOUSE_X }, + { "EVENT_MOUSE_Y", PA_PROP_EVENT_MOUSE_Y }, + { "EVENT_MOUSE_HPOS", PA_PROP_EVENT_MOUSE_HPOS }, + { "EVENT_MOUSE_VPOS", PA_PROP_EVENT_MOUSE_VPOS }, + { "EVENT_MOUSE_BUTTON", PA_PROP_EVENT_MOUSE_BUTTON }, + { "WINDOW_NAME", PA_PROP_WINDOW_NAME }, + { "WINDOW_ID", PA_PROP_WINDOW_ID }, + { "WINDOW_ICON", PA_PROP_WINDOW_ICON }, + { "WINDOW_ICON_NAME", PA_PROP_WINDOW_ICON_NAME }, + { "WINDOW_X", PA_PROP_WINDOW_X }, + { "WINDOW_Y", PA_PROP_WINDOW_Y }, + { "WINDOW_WIDTH", PA_PROP_WINDOW_WIDTH }, + { "WINDOW_HEIGHT", PA_PROP_WINDOW_HEIGHT }, + { "WINDOW_HPOS", PA_PROP_WINDOW_HPOS }, + { "WINDOW_VPOS", PA_PROP_WINDOW_VPOS }, + { "WINDOW_DESKTOP", PA_PROP_WINDOW_DESKTOP }, + { "WINDOW_X11_DISPLAY", PA_PROP_WINDOW_X11_DISPLAY }, + { "WINDOW_X11_SCREEN", PA_PROP_WINDOW_X11_SCREEN }, + { "WINDOW_X11_MONITOR", PA_PROP_WINDOW_X11_MONITOR }, + { "WINDOW_X11_XID", PA_PROP_WINDOW_X11_XID }, + { "APPLICATION_NAME", PA_PROP_APPLICATION_NAME }, + { "APPLICATION_ID", PA_PROP_APPLICATION_ID }, + { "APPLICATION_VERSION", PA_PROP_APPLICATION_VERSION }, + { "APPLICATION_ICON", PA_PROP_APPLICATION_ICON }, + { "APPLICATION_ICON_NAME", PA_PROP_APPLICATION_ICON_NAME }, + { "APPLICATION_LANGUAGE", PA_PROP_APPLICATION_LANGUAGE }, + { "APPLICATION_PROCESS_ID", PA_PROP_APPLICATION_PROCESS_ID }, + { "APPLICATION_PROCESS_BINARY", PA_PROP_APPLICATION_PROCESS_BINARY }, + { "APPLICATION_PROCESS_USER", PA_PROP_APPLICATION_PROCESS_USER }, + { "APPLICATION_PROCESS_HOST", PA_PROP_APPLICATION_PROCESS_HOST }, + { "APPLICATION_PROCESS_MACHINE_ID", PA_PROP_APPLICATION_PROCESS_MACHINE_ID}, + { "APPLICATION_PROCESS_SESSION_ID", PA_PROP_APPLICATION_PROCESS_SESSION_ID}, + { "DEVICE_STRING", PA_PROP_DEVICE_STRING }, + { "DEVICE_API", PA_PROP_DEVICE_API }, + { "DEVICE_DESCRIPTION", PA_PROP_DEVICE_DESCRIPTION }, + { "DEVICE_BUS_PATH", PA_PROP_DEVICE_BUS_PATH }, + { "DEVICE_SERIAL", PA_PROP_DEVICE_SERIAL }, + { "DEVICE_VENDOR_ID", PA_PROP_DEVICE_VENDOR_ID }, + { "DEVICE_VENDOR_NAME", PA_PROP_DEVICE_VENDOR_NAME }, + { "DEVICE_PRODUCT_ID", PA_PROP_DEVICE_PRODUCT_ID }, + { "DEVICE_PRODUCT_NAME", PA_PROP_DEVICE_PRODUCT_NAME }, + { "DEVICE_CLASS", PA_PROP_DEVICE_CLASS }, + { "DEVICE_FORM_FACTOR", PA_PROP_DEVICE_FORM_FACTOR }, + { "DEVICE_BUS", PA_PROP_DEVICE_BUS }, + { "DEVICE_ICON", PA_PROP_DEVICE_ICON }, + { "DEVICE_ICON_NAME", PA_PROP_DEVICE_ICON_NAME }, + { "DEVICE_ACCESS_MODE", PA_PROP_DEVICE_ACCESS_MODE }, + { "DEVICE_MASTER_DEVICE", PA_PROP_DEVICE_MASTER_DEVICE }, + { "DEVICE_BUFFERING_BUFFER_SIZE", PA_PROP_DEVICE_BUFFERING_BUFFER_SIZE }, + { "DEVICE_BUFFERING_FRAGMENT_SIZE", PA_PROP_DEVICE_BUFFERING_FRAGMENT_SIZE}, + { "DEVICE_PROFILE_NAME", PA_PROP_DEVICE_PROFILE_NAME }, + { "DEVICE_INTENDED_ROLES", PA_PROP_DEVICE_INTENDED_ROLES }, + { "DEVICE_PROFILE_DESCRIPTION", PA_PROP_DEVICE_PROFILE_DESCRIPTION }, + { "MODULE_AUTHOR", PA_PROP_MODULE_AUTHOR }, + { "MODULE_DESCRIPTION", PA_PROP_MODULE_DESCRIPTION }, + { "MODULE_USAGE", PA_PROP_MODULE_USAGE }, + { "MODULE_VERSION", PA_PROP_MODULE_VERSION }, + { "FORMAT_SAMPLE_FORMAT", PA_PROP_FORMAT_SAMPLE_FORMAT }, + { "FORMAT_RATE", PA_PROP_FORMAT_RATE }, + { "FORMAT_CHANNELS", PA_PROP_FORMAT_CHANNELS }, + { "FORMAT_CHANNEL_MAP", PA_PROP_FORMAT_CHANNEL_MAP }, + { "CONTEXT_FORCE_DISABLE_SHM", PA_PROP_CONTEXT_FORCE_DISABLE_SHM }, + { "BLUETOOTH_CODEC", PA_PROP_BLUETOOTH_CODEC }, + { NULL, NULL } +}; diff --git a/src/lua_libpulse_glib/pulseaudio.c b/src/lua_libpulse_glib/pulseaudio.c index 05eedb7..666305b 100644 --- a/src/lua_libpulse_glib/pulseaudio.c +++ b/src/lua_libpulse_glib/pulseaudio.c @@ -1,53 +1,42 @@ -/// libpulse bindings. -// -// @module pulseaudio +#include "pulseaudio.h" +#include "context.h" +#include "lua_util.h" +#include "proplist.h" +#include "volume.h" + +#include #include #include -#include #include -#include "pulseaudio.h" -#include "context.h" -#define LUA_MOD_EXPORT extern #define LUA_PULSEAUDIO "pulseaudio" - #if LUA_VERSION_NUM <= 501 // Shamelessly copied from Lua 5.3 source. // TODO: What's the official way to do this in 5.1? -void luaL_setfuncs (lua_State *L, const luaL_Reg *l, int nup) { +void luaL_setfuncs(lua_State* L, const luaL_Reg* l, int nup) { luaL_checkstack(L, nup, "too many upvalues"); - for (; l->name != NULL; l++) { /* fill the table with given functions */ - if (l->func == NULL) /* place holder? */ + for (; l->name != NULL; l++) { /* fill the table with given functions */ + if (l->func == NULL) /* place holder? */ lua_pushboolean(L, 0); else { int i; - for (i = 0; i < nup; i++) /* copy upvalues to the top */ + for (i = 0; i < nup; i++) /* copy upvalues to the top */ lua_pushvalue(L, -nup); - lua_pushcclosure(L, l->func, nup); /* closure with those upvalues */ + lua_pushcclosure(L, l->func, nup); /* closure with those upvalues */ } lua_setfield(L, -(nup + 2), l->name); } - lua_pop(L, nup); /* remove upvalues */ + lua_pop(L, nup); /* remove upvalues */ } -#define luaL_newlib(L,l) (luaL_register(L,LUA_PULSEAUDIO,l)) +#define luaL_newlib(L, l) (luaL_register(L, LUA_PULSEAUDIO, l)) #endif -/** - * Creates a new PulseAudio object. - * - * The API requires a GLib Main Context internally. Currently, only the default context - * is supported. - * - * @return[type=PulseAudio] - */ -int -pulseaudio_new(lua_State* L) -{ +int pulseaudio_new(lua_State* L) { GMainContext* ctx = g_main_context_default(); if (ctx == NULL) { lua_pushfstring(L, "Failed to accquire default GLib Main Context. Are we running in a Main Loop?"); @@ -55,7 +44,7 @@ pulseaudio_new(lua_State* L) return 0; } - pulseaudio* pa = lua_newuserdata (L, sizeof(pulseaudio)); + pulseaudio* pa = lua_newuserdata(L, sizeof(pulseaudio)); if (!pa) { return luaL_error(L, "failed to create pulseaudio userdata"); } @@ -68,12 +57,10 @@ pulseaudio_new(lua_State* L) } -/** +/* * Proxies table index operations to our metatable. */ -int -pulseaudio__index(lua_State* L) -{ +int pulseaudio__index(lua_State* L) { const char* index = luaL_checkstring(L, 2); luaL_getmetatable(L, LUA_PULSEAUDIO); lua_getfield(L, -1, index); @@ -81,28 +68,98 @@ pulseaudio__index(lua_State* L) } -/** +/* * Free the PulseAudio object */ -int -pulseaudio__gc(lua_State* L) -{ +int pulseaudio__gc(lua_State* L) { pulseaudio* pa = luaL_checkudata(L, 1, LUA_PULSEAUDIO); pa_glib_mainloop_free(pa->mainloop); return 0; } -int -pulseaudio_new_context(lua_State* L) -{ +int pulseaudio_new_context(lua_State* L) { pulseaudio* pa = luaL_checkudata(L, 1, LUA_PULSEAUDIO); return context_new(L, pa_glib_mainloop_get_api(pa->mainloop)); } -LUA_MOD_EXPORT int luaopen_lua_libpulse_glib(lua_State* L) -{ +void createlib_volume(lua_State* L) { + luaL_newmetatable(L, LUA_PA_VOLUME); + + lua_createtable(L, 0, sizeof volume_f / sizeof volume_f[0]); + luaL_setfuncs(L, volume_f, 0); + lua_setfield(L, -2, "__index"); + + luaL_setfuncs(L, volume_mt, 0); + +#if LUA_VERSION_NUM <= 501 + luaL_register(L, LUA_PA_VOLUME, volume_lib); +#else + luaL_newlib(L, volume_lib); +#endif + lua_setmetatable(L, -2); +} + + +void createlib_proplist(lua_State* L) { + luaL_newmetatable(L, LUA_PA_PROPLIST); + + lua_createtable(L, 0, sizeof proplist_f / sizeof proplist_f[0]); + luaL_setfuncs(L, proplist_f, 0); + lua_setfield(L, -2, "__index"); + + luaL_setfuncs(L, proplist_mt, 0); + +#if LUA_VERSION_NUM <= 501 + luaL_register(L, LUA_PA_PROPLIST, proplist_lib); +#else + luaL_newlib(L, proplist_lib); +#endif + + // Create a metatable with an `__index` table for read-only enum fields. + lua_createtable(L, 0, 1); + + lua_createtable(L, 0, sizeof proplist_enum / sizeof proplist_enum[0]); + for (int i = 0; proplist_enum[i].name != NULL; ++i) { + lua_pushstring(L, proplist_enum[i].value); + lua_setfield(L, -2, proplist_enum[i].name); + } + + lua_setfield(L, -2, "__index"); + lua_setmetatable(L, -2); +} + + +void createlib_context(lua_State* L) { + luaL_newmetatable(L, LUA_PA_CONTEXT); + + lua_createtable(L, 0, sizeof context_f / sizeof context_f[0]); + luaL_setfuncs(L, context_f, 0); + lua_setfield(L, -2, "__index"); + + luaL_setfuncs(L, context_mt, 0); +} + + +void createlib_pulseaudio(lua_State* L) { + luaL_newmetatable(L, LUA_PULSEAUDIO); + + lua_createtable(L, 0, sizeof pulseaudio_f / sizeof pulseaudio_f[0]); + luaL_setfuncs(L, pulseaudio_f, 0); + lua_setfield(L, -2, "__index"); + + luaL_setfuncs(L, pulseaudio_mt, 0); + +#if LUA_VERSION_NUM <= 501 + luaL_register(L, LUA_PULSEAUDIO, pulseaudio_lib); +#else + luaL_newlib(L, pulseaudio_lib); +#endif +} + + +LUA_MOD_EXPORT int luaopen_lua_libpulse_glib(lua_State* L) { // Create a table to store callback refs in, stored in the Lua registry lua_pushstring(L, LUA_PULSEAUDIO); lua_newtable(L); @@ -111,12 +168,9 @@ LUA_MOD_EXPORT int luaopen_lua_libpulse_glib(lua_State* L) lua_settable(L, -3); lua_rawset(L, LUA_REGISTRYINDEX); - luaL_newmetatable(L, LUA_PA_CONTEXT); - luaL_setfuncs(L, context_mt, 0); - - luaL_newmetatable(L, LUA_PULSEAUDIO); - luaL_setfuncs(L, pulseaudio_mt, 0); - - luaL_newlib(L, pulseaudio_lib); + createlib_volume(L); + createlib_context(L); + createlib_proplist(L); + createlib_pulseaudio(L); return 1; } diff --git a/src/lua_libpulse_glib/pulseaudio.h b/src/lua_libpulse_glib/pulseaudio.h index dd18fe0..e329771 100644 --- a/src/lua_libpulse_glib/pulseaudio.h +++ b/src/lua_libpulse_glib/pulseaudio.h @@ -1,17 +1,15 @@ +/** Bindings for PulseAudio's libpulse, using the GLib Main Loop. + * + * @module pulseaudio + */ #pragma once -#include "lua.h" #include "lauxlib.h" +#include "lua.h" + #include -#ifdef _WIN32 -#define LUA_MOD_EXPORT __declspec(dllexport) -#else -#define LUA_MOD_EXPORT extern -#endif - - -#define LUA_PULSEAUDIO "pulseaudio" +#define LUA_PULSEAUDIO "pulseaudio" #define LUA_PA_REGISTRY "pulseaudio.registry" @@ -20,25 +18,43 @@ typedef struct pulseaudio { } pulseaudio; -int -pulseaudio_new(lua_State*); -int -pulseaudio__gc(lua_State*); -int -pulseaudio__index(lua_State*); -int -pulseaudio_new_context(lua_State*); +/** Creates a new PulseAudio object. + * + * @function new + * @return[type=PulseAudio] + */ +int pulseaudio_new(lua_State*); -static const struct luaL_Reg pulseaudio_mt [] = { - {"__index", pulseaudio__index}, +int pulseaudio__gc(lua_State*); + + +/// PulseAudio API +/// @type PulseAudio + + +/** Creates a new PulseAudio context + * + * @function context + * @tparam string name The application name. + * @return[type=Context] + */ +int pulseaudio_new_context(lua_State*); + + +static const struct luaL_Reg pulseaudio_mt[] = { {"__gc", pulseaudio__gc}, + { NULL, NULL } +}; + + +static const struct luaL_Reg pulseaudio_f[] = { {"context", pulseaudio_new_context}, - {NULL, NULL} + { NULL, NULL } }; -static const struct luaL_Reg pulseaudio_lib [] = { +static const struct luaL_Reg pulseaudio_lib[] = { {"new", pulseaudio_new}, - {NULL, NULL} + { NULL, NULL } }; diff --git a/src/lua_libpulse_glib/volume.c b/src/lua_libpulse_glib/volume.c new file mode 100644 index 0000000..73c3466 --- /dev/null +++ b/src/lua_libpulse_glib/volume.c @@ -0,0 +1,307 @@ +#include "volume.h" + +#include +#include +#include +#include + + +int volume_to_lua(lua_State* L, const pa_cvolume* pa_volume) { + volume_t* volume = lua_newuserdata(L, sizeof(volume_t)); + if (pa_volume == NULL) { + lua_pushfstring(L, "Failed to allocate volume userdata"); + return lua_error(L); + } + luaL_getmetatable(L, LUA_PA_VOLUME); + lua_setmetatable(L, -2); + + volume->inner = *pa_volume; + return 1; +} + + +// When the value is a plain Lua table, a new `pa_cvolume` is allocated and the caller is +// responsible of freeing it with `pa_xfree`. +// If a userdata is passed instead, memory is owned by Lua and not be freed. +// TODO: Maybe construct userdata from the table, so that memory management is always handled +// by GC. Makes this easier to use. But only do so after the current version has been committed +// once, just so it stays available. +pa_cvolume* volume_from_lua(lua_State* L, int index) { + switch (lua_type(L, 2)) { + case LUA_TTABLE: { + uint8_t channels = (uint8_t) lua_objlen(L, index); + if (channels > PA_CHANNELS_MAX) { + channels = PA_CHANNELS_MAX; + } + + pa_cvolume* volume = pa_xnew(pa_cvolume, 1); + volume->channels = channels; + + for (int i = 0; i < channels; ++i) { + lua_pushinteger(L, i + 1); + lua_gettable(L, index); + pa_volume_t vol = (pa_volume_t) luaL_checkinteger(L, -1); + volume->values[i] = PA_CLAMP_VOLUME(vol); + lua_pop(L, 1); + } + + return volume; + } + case LUA_TUSERDATA: { + volume_t* volume = luaL_checkudata(L, index, LUA_PA_VOLUME); + return &volume->inner; + } + default: { + luaL_argerror(L, index, "expected table or userdata"); + return NULL; + } + } +} + + +int volume__len(lua_State* L) { + volume_t* volume = luaL_checkudata(L, 1, LUA_PA_VOLUME); + lua_pushinteger(L, volume->inner.channels); + return 1; +} + + +int volume__eq(lua_State* L) { + volume_t* left = luaL_checkudata(L, 1, LUA_PA_VOLUME); + volume_t* right = luaL_checkudata(L, 2, LUA_PA_VOLUME); + lua_pushboolean(L, pa_cvolume_equal(&left->inner, &right->inner)); + return 1; +} + + +int volume__index(lua_State* L) { + volume_t* volume = luaL_checkudata(L, 1, LUA_PA_VOLUME); + int index = luaL_checkint(L, 2); + luaL_argcheck(L, index >= 1 && index <= (PA_CHANNELS_MAX + 1), 2, "channel index out of bounds"); + lua_pushinteger(L, volume->inner.values[index - 1]); + return 1; +} + + +int volume__newindex(lua_State* L) { + volume_t* volume = luaL_checkudata(L, 1, LUA_PA_VOLUME); + lua_Integer index = luaL_checkinteger(L, 2); + lua_Integer value = luaL_checkinteger(L, 3); + luaL_argcheck(L, index >= 1 && index <= (PA_CHANNELS_MAX + 1), 2, "channel index out of bounds"); + luaL_argcheck(L, PA_VOLUME_IS_VALID(value), 3, "volume value is invalid"); + volume->inner.values[index - 1] = value; + return 0; +} + + +int volume__tostring(lua_State* L) { + volume_t* volume = luaL_checkudata(L, 1, LUA_PA_VOLUME); + char s[PA_CVOLUME_SNPRINT_MAX]; + pa_cvolume_snprint(s, PA_CVOLUME_SNPRINT_MAX, &volume->inner); + lua_pushstring(L, s); + return 1; +} + +int volume_is_valid(lua_State* L) { + // Lua's `checkudata` throws an error, and catching that is expensive. + // So we have to manually implement the check. + volume_t* volume = lua_touserdata(L, 1); + lua_getfield(L, LUA_REGISTRYINDEX, LUA_PA_VOLUME); + bool is_userdata = volume != NULL && lua_getmetatable(L, 1) && lua_rawequal(L, -1, -2); + // Remove the two metatables + lua_pop(L, 2); + lua_pushboolean(L, is_userdata && pa_cvolume_valid(&volume->inner)); + return 1; +} + + +int volume_channels_equal_to(lua_State* L) { + volume_t* volume = luaL_checkudata(L, 1, LUA_PA_VOLUME); + lua_Integer value = luaL_checkinteger(L, 2); + + luaL_argcheck(L, PA_VOLUME_IS_VALID(value), 2, "volume out of bounds"); + + lua_pushboolean(L, pa_cvolume_channels_equal_to(&volume->inner, (pa_volume_t) value)); + return 1; +} + + +int volume_is_muted(lua_State* L) { + volume_t* volume = luaL_checkudata(L, 1, LUA_PA_VOLUME); + lua_pushboolean(L, pa_cvolume_is_muted(&volume->inner)); + return 1; +} + + +int volume_is_norm(lua_State* L) { + volume_t* volume = luaL_checkudata(L, 1, LUA_PA_VOLUME); + lua_pushboolean(L, pa_cvolume_is_norm(&volume->inner)); + return 1; +} + + +int volume_set_channels(lua_State* L) { + volume_t* volume = luaL_checkudata(L, 1, LUA_PA_VOLUME); + lua_Integer channels = luaL_checkinteger(L, 2); + lua_Integer value = luaL_checkinteger(L, 3); + + luaL_argcheck(L, channels >= 0 && channels <= PA_CHANNELS_MAX, 2, "channel count out of bounds"); + luaL_argcheck(L, PA_VOLUME_IS_VALID(value), 3, "volume out of bounds"); + + pa_cvolume_set(&volume->inner, (unsigned int) channels, (pa_volume_t) value); + return 0; +} + + +int volume_set(lua_State* L) { + volume_t* volume = luaL_checkudata(L, 1, LUA_PA_VOLUME); + lua_Integer index = luaL_checkinteger(L, 2) - 1; + lua_Integer value = luaL_checkinteger(L, 3); + + luaL_argcheck(L, index >= 0 && index <= PA_CHANNELS_MAX, 2, "channel index out of bounds"); + luaL_argcheck(L, PA_VOLUME_IS_VALID(value), 3, "volume out of bounds"); + + volume->inner.values[index] = (pa_volume_t) value; + return 0; +} + + +int volume_get(lua_State* L) { + volume_t* volume = luaL_checkudata(L, 1, LUA_PA_VOLUME); + lua_Integer index = luaL_checkinteger(L, 2) - 1; + + luaL_argcheck(L, index > 0 && index < volume->inner.channels, 2, "channel index out of bounds"); + + lua_pushinteger(L, volume->inner.values[index]); + return 1; +} + + +int volume_reset(lua_State* L) { + volume_t* volume = luaL_checkudata(L, 1, LUA_PA_VOLUME); + lua_Integer channels = luaL_checkinteger(L, 2); + luaL_argcheck(L, channels >= 0 && channels <= PA_CHANNELS_MAX, 2, "channel count out of bounds"); + pa_cvolume_reset(&volume->inner, channels); + return 0; +} + + +int volume_mute(lua_State* L) { + volume_t* volume = luaL_checkudata(L, 1, LUA_PA_VOLUME); + lua_Integer channels = luaL_checkinteger(L, 2); + luaL_argcheck(L, channels >= 0 && channels <= PA_CHANNELS_MAX, 2, "channel count out of bounds"); + pa_cvolume_mute(&volume->inner, channels); + return 0; +} + + +int volume_avg(lua_State* L) { + volume_t* volume = luaL_checkudata(L, 1, LUA_PA_VOLUME); + lua_pushinteger(L, pa_cvolume_avg(&volume->inner)); + return 1; +} + + +int volume_min(lua_State* L) { + volume_t* volume = luaL_checkudata(L, 1, LUA_PA_VOLUME); + lua_pushinteger(L, pa_cvolume_min(&volume->inner)); + return 1; +} + + +int volume_max(lua_State* L) { + volume_t* volume = luaL_checkudata(L, 1, LUA_PA_VOLUME); + lua_pushinteger(L, pa_cvolume_max(&volume->inner)); + return 1; +} + + +int volume_inc(lua_State* L) { + volume_t* volume = luaL_checkudata(L, 1, LUA_PA_VOLUME); + lua_Integer value = luaL_checkinteger(L, 2); + luaL_argcheck(L, PA_VOLUME_IS_VALID(value), 2, "volume out of bounds"); + if (!pa_cvolume_inc(&volume->inner, (pa_volume_t) value)) { + return luaL_error(L, "failed to increase volume"); + } + + return 0; +} + + +int volume_dec(lua_State* L) { + volume_t* volume = luaL_checkudata(L, 1, LUA_PA_VOLUME); + lua_Integer value = luaL_checkinteger(L, 2); + luaL_argcheck(L, PA_VOLUME_IS_VALID(value), 2, "volume out of bounds"); + if (!pa_cvolume_dec(&volume->inner, (pa_volume_t) value)) { + return luaL_error(L, "failed to decrease volume"); + } + + return 0; +} + + +int volume_scale(lua_State* L) { + volume_t* volume = luaL_checkudata(L, 1, LUA_PA_VOLUME); + lua_Integer value = luaL_checkinteger(L, 2); + luaL_argcheck(L, PA_VOLUME_IS_VALID(value), 2, "volume out of bounds"); + if (!pa_cvolume_scale(&volume->inner, (pa_volume_t) value)) { + return luaL_error(L, "failed to scale volume"); + } + + return 0; +} + + +int volume_multiply(lua_State* L) { + volume_t* left = luaL_checkudata(L, 1, LUA_PA_VOLUME); + + switch (lua_type(L, 2)) { + case LUA_TNUMBER: { + lua_Integer value = luaL_checkinteger(L, 2); + luaL_argcheck(L, PA_VOLUME_IS_VALID(value), 2, "volume out of bounds"); + if (!pa_sw_cvolume_multiply_scalar(&left->inner, &left->inner, (pa_volume_t) value)) { + return luaL_error(L, "failed to multiply volume"); + } + + return 0; + } + case LUA_TUSERDATA: { + volume_t* right = luaL_checkudata(L, 2, LUA_PA_VOLUME); + if (!pa_sw_cvolume_multiply(&left->inner, &left->inner, &right->inner)) { + return luaL_error(L, "failed to multiply volume"); + } + + return 0; + } + default: { + return luaL_argerror(L, 2, "expected number or userdata"); + } + } +} + +int volume_divide(lua_State* L) { + volume_t* left = luaL_checkudata(L, 1, LUA_PA_VOLUME); + + switch (lua_type(L, 2)) { + case LUA_TNUMBER: { + lua_Integer value = luaL_checkinteger(L, 2); + luaL_argcheck(L, PA_VOLUME_IS_VALID(value), 2, "volume out of bounds"); + if (!pa_sw_cvolume_divide_scalar(&left->inner, &left->inner, (pa_volume_t) value)) { + return luaL_error(L, "failed to divide volume"); + } + + return 0; + } + case LUA_TUSERDATA: { + volume_t* right = luaL_checkudata(L, 2, LUA_PA_VOLUME); + if (!pa_sw_cvolume_divide(&left->inner, &left->inner, &right->inner)) { + return luaL_error(L, "failed to divide volume"); + } + + return 0; + } + default: { + return luaL_argerror(L, 2, "expected number or userdata"); + } + } +} diff --git a/src/lua_libpulse_glib/volume.h b/src/lua_libpulse_glib/volume.h new file mode 100644 index 0000000..01f8631 --- /dev/null +++ b/src/lua_libpulse_glib/volume.h @@ -0,0 +1,237 @@ +/** Bindings for libpulse's `pa_cvolume`. + * + * Contrary to libpulse, methods that change the a @{Volume} object generally don't return anything, but + * instead change the instance itself. + * + * @module pulseaudio.volume + */ +#ifndef volume_h_INCLUDED +#define volume_h_INCLUDED + +#include +#include +#include + +#define LUA_PA_VOLUME "pulseaudio.volume" + + +typedef struct volume_t { + pa_cvolume inner; +} volume_t; + + +/* Creates a Lua userdatum from/for a PulseAudio volume. + * + * Control over the `pa_plist`'s memory has to be taken over by this function, + * to enable integration into Lua's garbage collection. + * + * @function volume_to_lua + * @treturn Volume + */ +int volume_to_lua(lua_State*, const pa_cvolume*); + +/* Creates a pa_cvolume from Lua userdata + */ +pa_cvolume* volume_from_lua(lua_State*, int); + +/* Implements the `#` length operator. + * This simply proxies to the `.channels` value. + */ +int volume__len(lua_State*); +int volume__eq(lua_State*); +int volume__tostring(lua_State*); +int volume__index(lua_State*); +int volume__newindex(lua_State*); + + +/// Static Functions +/// @section static + + +/** Checks whether a value is a valid @{Volume}. + * + * @function is_valid + * @tparam any value The value to check + * @treturn boolean + */ +int volume_is_valid(lua_State*); + +/// Volume +/// @type Volume + +/** Returns the average volume over all channels. + * + * @function Volume:avg + * @treturn number + */ +int volume_avg(lua_State*); + +/** Returns `true` if the volume of all channels is equal to the specified value. + * + * @function Volume:channels_equal_to + * @tparam number value Volume to compare to + * @treturn boolean + */ +int volume_channels_equal_to(lua_State*); + +/** Decreases the volume by the given amount. + * + * The proportions between the channels are kept. + * + * @function Volume:dec + * @tparam number value The value to decrease by. + */ +int volume_dec(lua_State*); + +/** Divides the volume by the given value. + * + * The value to divide by may either be a scalar, that's applied to all channels, + * or another instance of @{Volume}, which would be applied channel by channel. + * + * It is possible to divide a @{Volume} by itself. + * + * This is only valid for software volumes. + * + * @function Volume:divide + * @tparam number|Volume value The volume to divide by. + */ +int volume_divide(lua_State*); + +/** Returns the volume of a single channel. + * + * @function Volume:get + * @tparam number index The channel index + * @treturn number The channel's volume + */ +int volume_get(lua_State*); + +/** Increases the volume by the given amount. + * + * The proportions between the channels are kept. + * + * @function Volume:inc + * @tparam number value the value to increase by. + */ +int volume_inc(lua_State*); + +/** Returns `true` when all channels are muted. + * + * @function Volume:is_muted + * @treturn boolean + */ +int volume_is_muted(lua_State*); + +/** Returns `true` when all channels are on normal level. + * + * @function Volume:is_norm + * @treturn boolean + */ +int volume_is_norm(lua_State*); + +/** Returns the maximum volume out of all channels. + * + * @function Volume:max + * @treturn number + */ +int volume_max(lua_State*); + +/** Returns the minimum volume out of all channels. + * + * @function Volume:min + * @treturn number + */ +int volume_min(lua_State*); + +/** Multiplies the volume by the given amount. + * + * The value to multiply with may either be a scalar, that's applied to all channels, + * or another instance of @{Volume}, which would be applied channel by channel. + * + * It is possible to multiply a @{Volume} by itself. + * + * This is only valid for software volumes. + * + * @function Volume:multiply + * @tparam number|Volume value + */ +int volume_multiply(lua_State*); + +/** Mutes all channels. + * + * @function Volume:mute + */ +int volume_mute(lua_State*); + +/** Resets all channels to normal volume. + * + * @function Volume:reset + */ +int volume_reset(lua_State*); + +/** Scales all channels to the passed amount. + * + * This adjust all channel volumes so that the maximum between them equals the given value, while + * keeping proportions between channels the same. + * + * @function Volume:scale + * @tparam number value The value to scale to. + */ +int volume_scale(lua_State*); + +/** Sets a channel to the given value. + * + * @function Volume:set + * @tparam number index The channel index. + * @tparam number value The volume to set to. + */ +int volume_set(lua_State*); + +/** Sets a number of channels to the given volume value. + * + * @function Volume:set_channels + * @tparam number channels The number of channels to set + * @tparam number value The volume to set to. + */ +int volume_set_channels(lua_State*); + + +static const struct luaL_Reg volume_f[] = { + {"avg", volume_avg }, + { "channels_equal_to", volume_channels_equal_to}, + { "dec", volume_dec }, + { "divide", volume_divide }, + { "get", volume_get }, + { "inc", volume_inc }, + { "is_muted", volume_is_muted }, + { "is_norm", volume_is_norm }, + { "is_valid", volume_is_valid }, + { "max", volume_max }, + { "min", volume_min }, + { "multiply", volume_multiply }, + { "mute", volume_mute }, + { "reset", volume_reset }, + { "scale", volume_scale }, + { "set", volume_set }, + { NULL, NULL } +}; + + +// We don't add `__index` here, as it needs to be handled specially, +// to allow indexing both by channel index as well as accessing the methods above. +static const struct luaL_Reg volume_mt[] = { + {"__len", volume__len }, + { "__tostring", volume__tostring}, + { "__eq", volume__eq }, + { "__newindex", volume__newindex}, + { NULL, NULL } +}; + + +static const struct luaL_Reg volume_lib[] = { + {"is_valid", volume_is_valid}, + { NULL, NULL } +}; + + +#endif // volume_h_INCLUDED + diff --git a/tools/process_docs.sh b/tools/process_docs.sh index 9b5fd46..f6ab73f 100644 --- a/tools/process_docs.sh +++ b/tools/process_docs.sh @@ -14,7 +14,7 @@ run() { "$@" } -find src -iname '*.lua' -or -iname '*.c' -not -path '*/internal/*' | while read -r f; do +find src -iname '*.lua' -or -iname '*.c' -or -iname '*.h' -not -path '*/internal/*' | while read -r f; do mkdir -p "$(dirname "$OUT/$f")" run "$LUA" ./tools/preprocessor.lua "$f" "$OUT/$f" done