1
Fork 0

Compare commits

..

No commits in common. "wip/lgi" and "master" have entirely different histories.

41 changed files with 4679 additions and 365 deletions

View file

@ -1,7 +1,6 @@
return {
default = {
verbose = true,
helper = "./spec/_helper.lua",
lpath = "./src/?.lua;./src/?/init.lua;./src/?/?.lua;./tests/?.lua",
},
}

22
.ci/deploy-docs.sh Executable file
View file

@ -0,0 +1,22 @@
#!/bin/sh
set -ex
mkdir -p "${BUILD_DIR:-./build}"
CMAKE_ARGS=""
if [ -n "$CI" ]; then
CMAKE_ARGS="-DCI=ON"
fi
sudo enable-lua ${LUA_VERSION:-5.1}
make -j $(nproc) -C ${SOURCE_DIR:-.} BUILD_DIR=${BUILD_DIR:-./build} LUA_VERSION=${LUA_VERSION:-5.1} LUA=lua doc
tar -czf doc.tar.gz -C ${BUILD_DIR:-./build}/doc .
curl -T doc.tar.gz -XPOST --user ${DOCS_CREDENTIALS} https://${DOCS_HOST}/publish/lua-libpulse-glib
sudo disable-lua ${LUA_VERSION:-5.1}

22
.ci/deploy-docs.yml Normal file
View file

@ -0,0 +1,22 @@
---
platform: linux
image_resource:
type: registry-image
source:
repository: registry.local:5000/lua-clib-pulse
tag: latest
inputs:
- name: repo
params:
CI: true
SOURCE_DIR: repo
BUILD_DIR: /tmp/build-output
DOCS_HOST: ((doc-host))
DOCS_CREDENTIALS: "((deploy-user)):((deploy-password))"
run:
path: repo/.ci/deploy-docs.sh

68
.ci/pipeline.yml Normal file
View file

@ -0,0 +1,68 @@
---
resources:
- name: repo
type: git
icon: github
source:
uri: https://git.sclu1034.dev/lucas/lua-libpulse-glib
branch: master
jobs:
- name: test
plan:
- get: repo
trigger: true
- in_parallel:
- task: run-test-5.1
file: repo/.ci/test.yml
vars:
lua-version: 5.1
- task: run-test-5.2
file: repo/.ci/test.yml
vars:
lua-version: 5.2
- task: run-test-5.3
file: repo/.ci/test.yml
vars:
lua-version: 5.3
# There is no rock for 5.4 LGI, yet. And I don't want to build that from source.
- try:
task: run-test-5.4
file: repo/.ci/test.yml
vars:
lua-version: 5.4
- name: rock
plan:
- get: repo
trigger: true
- in_parallel:
- task: run-rock-5.1
file: repo/.ci/rock.yml
vars:
lua-version: 5.1
- task: run-rock-5.2
file: repo/.ci/rock.yml
vars:
lua-version: 5.2
- task: run-rock-5.3
file: repo/.ci/rock.yml
vars:
lua-version: 5.3
# There is no rock for 5.4 LGI, yet. And I don't want to build that from source.
- try:
task: run-rock-5.4
file: repo/.ci/rock.yml
vars:
lua-version: 5.4
- name: doc
plan:
- get: repo
trigger: true
- task: deploy-docs
file: repo/.ci/deploy-docs.yml
vars:
doc-host: ((doc-host))
deploy-user: ((deploy-user))
deploy-password: ((deploy-password))

14
.ci/rock.sh Executable file
View file

@ -0,0 +1,14 @@
#!/bin/sh
set -ex
sudo enable-lua ${LUA_VERSION:-5.1}
eval "$(luarocks --lua-version ${LUA_VERSION:-5.1} path)"
luarocks --lua-version ${LUA_VERSION:-5.1} install lgi
luarocks --lua-version ${LUA_VERSION:-5.1} install ldoc
luarocks --lua-version ${LUA_VERSION:-5.1} install lua-discount
luarocks --lua-version ${LUA_VERSION:-5.1} make rocks/lua-libpulse-glib-scm-1.rockspec
sudo disable-lua ${LUA_VERSION:-5.1}

20
.ci/rock.yml Normal file
View file

@ -0,0 +1,20 @@
---
platform: linux
image_resource:
type: registry-image
source:
repository: registry.local:5000/lua-clib-pulse
tag: latest
inputs:
- name: repo
params:
CI: true
LUA_VERSION: ((lua-version))
run:
path: .ci/rock.sh
dir: repo

21
.ci/test.sh Executable file
View file

@ -0,0 +1,21 @@
#!/bin/sh
set -ex
mkdir -p "${BUILD_DIR:-./build}"
CMAKE_ARGS=""
if [ -n "$CI" ]; then
CMAKE_ARGS="-DCI=ON"
fi
sudo enable-lua ${LUA_VERSION:-5.1}
eval "$(luarocks --lua-version ${LUA_VERSION:-5.1} path)"
luarocks --lua-version ${LUA_VERSION:-5.1} install busted
luarocks --lua-version ${LUA_VERSION:-5.1} install lgi
make -j $(nproc) -C ${SOURCE_DIR:-.} BUILD_DIR=${BUILD_DIR:-./build} LUA_VERSION=${LUA_VERSION:-5.1} LUA=lua build
sudo disable-lua ${LUA_VERSION:-5.1}

20
.ci/test.yml Normal file
View file

@ -0,0 +1,20 @@
---
platform: linux
image_resource:
type: registry-image
source:
repository: registry.local:5000/lua-clib-pulse
tag: latest
inputs:
- name: repo
params:
CI: true
SOURCE_DIR: repo
BUILD_DIR: /tmp/build-output
LUA_VERSION: ((lua-version))
run:
path: repo/.ci/test.sh

75
.clang-format Normal file
View file

@ -0,0 +1,75 @@
---
# kak: filetype=yaml
Language: Cpp
BasedOnStyle: LLVM
AlignArrayOfStructures: Left
AlignConsecutiveMacros: AcrossComments
AlignConsecutiveAssignments: None
AlignConsecutiveBitFields: AcrossComments
AlignConsecutiveDeclarations: None
AllowAllArgumentsOnNextLine: false
AllowAllParametersOfDeclarationOnNextLine: false
AllowShortEnumsOnASingleLine: false
AllowShortFunctionsOnASingleLine: None
AlwaysBreakTemplateDeclarations: Yes
AttributeMacros:
- __capability
BinPackArguments: false
BinPackParameters: false
BreakBeforeBinaryOperators: NonAssignment
BreakBeforeInheritanceComma: false
BreakInheritanceList: AfterComma
ColumnLimit: 120
Cpp11BracedListStyle: false
EmptyLineBeforeAccessModifier: Always
FixNamespaceComments: true
IncludeBlocks: Regroup
IncludeCategories:
- Regex: '^"(llvm|llvm-c|clang|clang-c)/'
Priority: 2
SortPriority: 0
CaseSensitive: false
- Regex: '^(<|"(gtest|gmock|isl|json)/)'
Priority: 3
SortPriority: 0
CaseSensitive: false
- Regex: '.*'
Priority: 1
SortPriority: 0
CaseSensitive: false
IncludeIsMainRegex: '(_test)?$'
IncludeIsMainSourceRegex: ''
IndentExternBlock: NoIndent
IndentWidth: 4
KeepEmptyLinesAtTheStartOfBlocks: false
MaxEmptyLinesToKeep: 2
# PackConstructorInitializers: CurrentLine
PointerAlignment: Left
ReferenceAlignment: Right
ReflowComments: true
# SeparateDefinitionBlocks: Leave
SpaceAfterCStyleCast: true
SpaceAfterLogicalNot: false
SpaceAfterTemplateKeyword: false
SpaceBeforeAssignmentOperators: true
SpaceBeforeCaseColon: false
SpaceBeforeCpp11BracedList: false
SpaceBeforeCtorInitializerColon: true
SpaceBeforeInheritanceColon: true
SpaceBeforeParens: ControlStatements
SpaceAroundPointerQualifiers: Default
SpaceBeforeRangeBasedForLoopColon: true
SpaceInEmptyBlock: false
SpaceInEmptyParentheses: false
SpacesBeforeTrailingComments: 1
SpacesInAngles: Never
SpacesInConditionalStatement: false
SpacesInContainerLiterals: true
SpacesInCStyleCastParentheses: false
SpacesInLineCommentPrefix:
Minimum: 1
Maximum: -1
SpacesInParentheses: false
SpacesInSquareBrackets: false
SpaceBeforeSquareBrackets: false
TabWidth: 4

2
.fdignore Normal file
View file

@ -0,0 +1,2 @@
out/
.sass-cache/

View file

@ -1,79 +0,0 @@
name: Lint & Test
on:
push:
branches: [ master, staging, trying ]
pull_request:
branches: [ master ]
env:
CI: true
DEBIAN: noninteractive
jobs:
check:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2.4.0
- uses: leafo/gh-actions-lua@v8.0.0
- uses: leafo/gh-actions-luarocks@v4.0.0
- name: Install LuaCheck
run: luarocks install luacheck
- name: Run checks
run: make check LUA_VERSION=${{ matrix.lua_version }}
test:
runs-on: ubuntu-20.04
strategy:
fail-fast: false
matrix:
include:
- lua_version: '5.1'
- lua_version: '5.2'
- lua_version: '5.3'
- lua_version: 'luajit'
steps:
- uses: actions/checkout@v2.4.0
- uses: leafo/gh-actions-lua@v8.0.0
with:
luaVersion: ${{ matrix.lua_version }}
- uses: leafo/gh-actions-luarocks@v4.0.0
- name: Install dependencies
shell: /bin/bash -o errexit -o pipefail -o xtrace {0}
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends gobject-introspection libgirepository1.0-dev libpulse-dev
luarocks install busted
luarocks install lgi
- name: Run tests
run: make test LUA_VERSION=${{ matrix.lua_version }}
rock:
runs-on: ubuntu-20.04
needs: [test, check]
strategy:
fail-fast: false
matrix:
include:
- lua_version: '5.1'
- lua_version: '5.2'
- lua_version: '5.3'
steps:
- uses: actions/checkout@v2.4.0
- uses: leafo/gh-actions-lua@v8.0.0
with:
luaVersion: ${{ matrix.lua_version }}
- uses: leafo/gh-actions-luarocks@v4.0.0
- name: Install dependencies
shell: /bin/bash -o errexit -o pipefail -o xtrace {0}
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends gobject-introspection libgirepository1.0-dev
- name: Make rock
run: make rock LUA_VERSION=${{ matrix.lua_version }}

View file

@ -1,34 +0,0 @@
name: Deploy docs
on:
push:
branches: ['master']
env:
DEBIAN: noninteractive
jobs:
deploy-docs:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2.4.0
- uses: leafo/gh-actions-lua@v8.0.0
- uses: leafo/gh-actions-luarocks@v4.0.0
- uses: actions/setup-node@v2
with:
node-version: 'lts/*'
- name: Install dependencies
run: |
luarocks install ldoc
luarocks install lua-discount
npm install --global sass
- name: Generate documentation
run: make doc
- name: Deploy to gh-pages
uses: JamesIves/github-pages-deploy-action@v4.2.5
with:
branch: gh-pages
folder: out/doc

10
.gitignore vendored
View file

@ -1,2 +1,12 @@
out/
.gdb_history
.gdbinit
lua.debug
.cache/clangd
compile_commands.json
.sass-cache
*.rockspec.asc
*.src.rock

3
.gitmodules vendored
View file

@ -1,3 +0,0 @@
[submodule "lib/lgi"]
path = lib/lgi
url = https://github.com/lgi-devs/lgi

View file

@ -1,21 +0,0 @@
{
"configurations": [
{
"name": "Linux",
"includePath": [
"${workspaceFolder}/**",
"/usr/include/lua5.3",
"/usr/include/glib-2.0",
"/usr/lib/glib-2.0/include",
"/usr/lib/x86_64-linux-gnu/glib-2.0/include",
"/usr/include/gobject-introspection-1.0"
],
"defines": [],
"compilerPath": "/usr/bin/gcc",
"cStandard": "c11",
"cppStandard": "c++14",
"intelliSenseMode": "linux-clang-x64"
}
],
"version": 4
}

10
.vscode/settings.json vendored
View file

@ -1,10 +0,0 @@
{
"C_Cpp.errorSquiggles": "Enabled",
"files.associations": {
"**/tasks/*.yml": "ansible",
"*.rockspec": "lua",
"*.ld": "lua",
"glib-mainloop.h": "c",
"lua.h": "c"
}
}

View file

@ -1,4 +1,4 @@
PROJECT = lgi_pulseaudio
PROJECT = lua_libpulse_glib
PREFIX ?= /usr/local
BUILD_DIR = out
@ -40,10 +40,11 @@ CCFLAGS += -Wall -g -rdynamic $(shell $(PKG_CONFIG) --cflags $(PKGS)) -I$(LUA_IN
LIBS = -L$(shell dirname "$(shell $(CC) -print-libgcc-file-name)") -L"$(LUA_LIBDIR)" -L"./"
LIBS += $(shell $(PKG_CONFIG) --libs $(PKGS))
OBJS = $(shell find src -type f -iname '*.c' | sed 's/\(.*\)\.c$$/$(BUILD_DIR)\/\1\.o/')
LGI_OBJS = $(shell find lib/lgi/lgi -type f -iname '*.c' | sed 's/\(.*\)\.c$$/\1\.o/')
TARGET = $(BUILD_DIR)/$(PROJECT).so
LUA_CPATH = $(shell echo "$${PWD}/$(BUILD_DIR)/?.so;$${LUA_CPATH}")
ifdef CI
CHECK_ARGS ?= --formatter TAP
TEST_ARGS ?= --output=TAP
@ -51,58 +52,67 @@ TEST_ARGS ?= --output=TAP
CCFLAGS += -Werror
endif
.PHONY: clean doc doc-content doc-styles install test check rock
bold := $(shell tput bold)
orange := $(shell tput setaf 7)
title := $(bold)$(orange)
reset := $(shell tput sgr0)
build: build-lgi $(TARGET)
.PHONY: all clean doc doc-content doc-styles install uninstall test check rock
build-lgi:
make -C lib/lgi
all: build doc
build: $(TARGET)
$(BUILD_DIR)/%.o: %.c
@mkdir -p $(shell dirname "$@")
$(CC) -c $(CCFLAGS) $< -o $@
@echo "$(title)$(CC) $< -o $@$(reset)"
@$(CC) -c $(CCFLAGS) $< -o $@
$(TARGET): $(OBJS)
$(CC) $(LIBFLAG) -o $@ $(OBJS) $(LGI_OBJS) $(LIBS)
@echo "$(title)$(CC) -o $@$(reset)"
@$(CC) $(LIBFLAG) -o $@ $(OBJS) $(LIBS)
doc-styles:
@printf "\e[1;97mGenerate stylesheet\e[0m\n"
sass doc/ldoc.scss $(BUILD_DIR)/doc/ldoc.css
doc-content:
$(BUILD_DIR)/doc/index.html:
@mkdir -p "$(BUILD_DIR)/doc" "$(BUILD_DIR)/src"
@printf "\e[1;97mPreprocess sources\e[0m\n"
@echo "$(title)Preprocess sources$(reset)"
sh tools/process_docs.sh "$(BUILD_DIR)"
@printf "\e[1;97mGenerate documentation\e[0m\n"
@echo "$(title)Generate documentation$(reset)"
ldoc --config=doc/config.ld --dir "$(BUILD_DIR)/doc" --project $(PROJECT) "$(BUILD_DIR)/src"
doc: doc-content doc-styles
ifdef CI
touch "$(BUILD_DIR)/doc/.nojekyll"
endif
$(BUILD_DIR)/doc/ldoc.css: doc/ldoc.scss
@mkdir -p "$(BUILD_DIR)/doc"
@echo "$(title)Generate stylesheet$(reset)"
sass doc/ldoc.scss $(BUILD_DIR)/doc/ldoc.css
doc-styles: $(BUILD_DIR)/doc/ldoc.css
doc-content: $(BUILD_DIR)/doc/index.html
doc: doc-styles doc-content
clean:
rm -r out/
install: build doc
@printf "\e[1;97mInstall C library\e[0m\n"
xargs install -vDm 644 -t $(INSTALL_LIBDIR)/$(PROJECT) $(TARGET)
@echo "$(title)Install C library\033[0m"
install -vDm 644 -t $(INSTALL_LIBDIR) $(TARGET)
# @printf "\e[1;97mInstall Lua libraries\e[0m\n"
# find src/ -type f -iname '*.lua' | xargs install -vDm 644 -t $(INSTALL_LUADIR)/$(PROJECT)
@printf "\e[1;97mInstall documentation\e[0m\n"
@echo "$(title)Install documentation\033[0m"
install -vd $(INSTALL_DOCDIR)
cp -vr $(BUILD_DIR)/doc/* $(INSTALL_DOCDIR)
uninstall:
rm $(INSTALL_LIBDIR)/$(PROJECT).so
rm -r $(INSTALL_DOCDIR)
check:
@echo "Nothing to do"
test:
spec: build
busted --config-file=.busted.lua --lua=$(LUA) $(TEST_ARGS)
rock:
luarocks --local --lua-version $(LUA_VERSION) make rocks/lgi-pulseaudio-scm-1.rockspec
test: build
$(LUA) test.lua
run: build
env LUA_CPATH="./out/?.so;${LUA_CPATH}" lua5.1 test.lua
rock:
luarocks --local --lua-version $(LUA_VERSION) make rocks/lua-libpulse-glib-scm-1.rockspec

View file

@ -1,11 +1,10 @@
= lgi-pulseaudio
= lua-libpulse-glib
:idprefix:
:idseparator: -
ifdef::env-github,env-browser[]
:toc: macro
:toclevels: 1
endif::[]
ifdef::env-github[]
:branch: master
:status:
:outfilesuffix: .adoc
@ -15,31 +14,56 @@ ifdef::env-github[]
:note-caption: :paperclip:
:tip-caption: :bulb:
:warning-caption: :warning:
endif::[]
:url-ci-github: https://github.com/sclu1034/lgi-pulseaudio/actions
:url-ci-badge-github: https://img.shields.io/github/workflow/status/sclu1034/lgi-pulseaudio/Lint%20&%20Test?style=flat-square
:url-ci: https://ci.sclu1034.dev/teams/main/pipelines/lua-libpulse-glib
:url-ci-badge: https://ci.sclu1034.dev/api/v1/teams/main/pipelines/lua-libpulse-glib/badge
:url-license-badge: https://img.shields.io/badge/license-GPLv3-brightgreen?style=flat-square
:url-luarocks-badge: https://img.shields.io/luarocks/v/sclu1034/lgi-pulseaudio?style=flat-square
:url-luarocks-link: https://luarocks.org/modules/sclu1034/lgi-pulseaudio
:url-luarocks-badge: https://img.shields.io/luarocks/v/sclu1034/lua-libpulse-glib?style=flat-square
:url-luarocks-link: https://luarocks.org/modules/sclu1034/lua-libpulse-glib
image:{url-license-badge}[License]
ifdef::status[]
image:{url-ci-badge-github}[Build Status (GitHub Actions), link={url-ci-github}]
image:{url-ci-badge}[Build Status (Concourse CI), link={url-ci}]
endif::[]
image:{url-luarocks-badge}[LuaRocks Package, link={url-luarocks-link}]
https://freedesktop.org/software/pulseaudio/doxygen/index.html[libpulse] bindings for use with a GLib MainLoop via
https://github.com/lgi-devs/lgi/[LGI].
== Installation
The project is still in a development state. It hasn't been fully tested yet, and not all functions from libpulse
have a corresponding binding yet.
Please do report anything that's missing or not working correctly.
_lgi-pulseaudio_ is available via LuaRocks:
== Quick Start
Install https://github.com/lgi-devs/lgi[lgi] and _lua_libpulse_glib_ from LuaRocks:
[source,shell]
----
luarocks install lgi-pulseaudio
luarocks install lgi
luarocks install --dev lua-libpulse-glib
----
When cloning/vendoring the library, the following dependencies are required:
[source,lua]
----
local lgi = require("lgi")
local pulseaudio = require("lua_libpulse_glib")
local ppretty = require("pl.pretty")
* https://github.com/lgi-devs/lgi[lgi]
local pa = pulseaudio.new()
local ctx = pa:context("My Test App")
local loop = lgi.GLib.MainLoop.new()
ctx:connect(nil, function(state)
if state == 4 then
print("Connection is ready")
ctx:get_sinks(function(sinks)
ppretty.dump(sinks)
loop:quit()
end)
end
end)
loop:run()
----

View file

@ -1,10 +0,0 @@
status = [
"check",
"test (5.1)",
"test (5.2)",
"test (5.3)",
"test (luajit)",
"rock (5.1)",
"rock (5.2)",
"rock (5.3)",
]

View file

@ -1,13 +1,49 @@
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.
local lgi = require("lgi")
local pulseaudio = require("lua_libpulse_glib")
local ppretty = require("pl.pretty")
local pa = pulseaudio.new()
local ctx = pa:context("My Test App")
local loop = lgi.GLib.MainLoop.new()
ctx:connect(nil, function(state)
if state == 4 then
print("Connection is ready")
ctx:get_sinks(function(sinks)
ppretty.dump(sinks)
loop:quit()
end)
end
end)
loop:run()
]]
template = true
format = 'discount'
pretty = 'lua'
prettify_files = 'show'
-- prettify_files = 'show'
backtick_references = false
wrap = true
no_space_before_args = true

@ -1 +0,0 @@
Subproject commit 340a250ab0dfc157fe027f7a29eafda9b8572e5c

View file

@ -1,20 +1,18 @@
package = "lgi-pulseaudio"
package = "lua-libpulse-glib"
version = "scm-1"
source = {
url = "git://github.com/sclu1034/lgi-pulseaudio.git"
url = "git://github.com/sclu1034/lua-libpulse-glib.git"
}
description = {
summary = "An asynchronous high(er)-level API wrapper for LGI",
homepage = "https://github.com/sclu1034/lgi-pulseaudio",
summary = "Lua bindings for PulseAudio's libpulse, using the GLib Main Loop.",
homepage = "https://github.com/sclu1034/lua-libpulse-glib",
license = "GPLv3"
}
dependencies = {
"lua >= 5.1",
"lgi",
"async.lua"
"lua >= 5.1"
}
build = {

0
spec/.gitkeep Normal file
View file

View file

@ -1,143 +0,0 @@
/// libpulse bindings.
//
// @module pulseaudio
#include "lib/lgi/lgi/lgi.h"
#undef G_LOG_DOMAIN
#define G_LOG_DOMAIN "lgi-pulseaudio"
#include <stdlib.h>
#include <string.h>
#include <lua.h>
#include <lualib.h>
#include <lauxlib.h>
#include <glib-2.0/glib.h>
#include <glib-object.h>
#include <pulse/glib-mainloop.h>
#ifdef _WIN32
#define LUA_MOD_EXPORT __declspec(dllexport)
#else
#define LUA_MOD_EXPORT extern
#endif
#define LUA_PULSEAUDIO "pulseaudio"
#if LUA_VERSION_NUM <= 501
// Shamelessly copied from Lua 5.3 source.
// TODO: Is there any official way to do this in 5.1?
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? */
lua_pushboolean(L, 0);
else {
int i;
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_setfield(L, -(nup + 2), l->name);
}
lua_pop(L, nup); /* remove upvalues */
}
#define luaL_newlib(L,l) (luaL_register(L,LUA_PULSEAUDIO,l))
#endif
typedef struct pulseaudio {
pa_glib_mainloop* mainloop;
} pulseaudio;
static int
pulseaudio_new(lua_State* L)
{
printf("get new stuff: %d\n", lua_gettop(L));
GMainContext* ctx = NULL;
if (lua_gettop(L) > 0) {
luaL_argcheck(L, lua_isuserdata(L, 1), 1, NULL);
printf("it's userdata\n");
lua_getfield(L, -1, "type");
GITypeInfo* type_info = *(GITypeInfo **) luaL_checkudata(L, -1, LGI_GI_INFO);
luaL_argcheck(L, type_info != NULL && GI_IS_TYPE_INFO(type_info), 1, "paramter has no type info");
// TODO: Check if it's really a GMainContext
printf("type: %s\n", g_base_info_get_name(type_info));
// Pop the `type` field
lua_pop(L, 1);
lua_pop(L, lgi_marshal_2c(L, type_info, NULL, GI_TRANSFER_NOTHING, (gpointer) ctx, 1, 0, NULL, NULL));
ctx = (GMainContext*) lua_touserdata(L, 1);
} else {
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?");
lua_error(L);
return 0;
}
}
printf("got some context\n");
pulseaudio* pa = lua_newuserdata (L, sizeof(pulseaudio));
if (!pa) {
return luaL_error(L, "failed to create pulseaudio userdata");
}
pa->mainloop = pa_glib_mainloop_new(ctx);
luaL_getmetatable(L, LUA_PULSEAUDIO);
lua_setmetatable(L, -2);
return 1;
}
// TODO: Implement `__gc` meta method to free the inner `pa_glib_mainloop`
static const struct luaL_Reg pulseaudio_mt [] = {
{NULL, NULL}
};
static const struct luaL_Reg pulseaudio_lib [] = {
{"new", pulseaudio_new},
{NULL, NULL}
};
// Shamelessly stolen from Lua 5.3 source code
// Uses Lua's `require`
static int dolibrary(lua_State *L, const char *name) {
int status;
int base;
base = lua_gettop(L);
lua_getglobal(L, "require");
lua_pushstring(L, name);
status = lua_pcall(L, 1, 1, 0);
// Data for the `require` call is no longer needed
lua_insert(L, base);
lua_pop(L, 2);
if (status == 0) {
lua_setglobal(L, name);
lua_pop(L, 1);
}
return status;
}
LUA_MOD_EXPORT int luaopen_lgi_pulseaudio(lua_State* L)
{
if (dolibrary(L, "lgi") != 0) {
lua_pushfstring(L, "failed to load library 'lgi'");
lua_error(L);
return 0;
}
luaL_newmetatable(L, LUA_PULSEAUDIO);
luaL_setfuncs(L, pulseaudio_mt, 0);
luaL_newlib(L, pulseaudio_lib);
return 1;
}

View file

@ -0,0 +1,62 @@
#include "callback.h"
#include "pulseaudio.h"
simple_callback_data* prepare_lua_callback(lua_State* L, int callback_index) {
// 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);
if (callback_index != 0) {
// Copy the callback function to the thread's stack
luaL_checktype(L, callback_index, LUA_TFUNCTION);
lua_pushvalue(L, callback_index);
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;
// Clean up the intermediate data from creating the thread
lua_pop(L, 2);
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);
}
// When preparing a thread for this callback, the function must be at index `1`.
// Values on the stack after that will be ignored. This allows adding userdata and other values
// for the purpose of memory management, to keep them alive until the callback has been called.
void success_callback(pa_context* c, int success, void* userdata) {
simple_callback_data* data = (simple_callback_data*) userdata;
lua_State* L = data->L;
// Copy the callback function to a position from where it can be called.
// There may be other values on the stack for memory management.
lua_pushvalue(L, 1);
lua_pushnil(L);
lua_pushboolean(L, success);
lua_call(L, 2, 0);
free_lua_callback(data);
}

View file

@ -0,0 +1,40 @@
#pragma once
#include <lua.h>
#include <pulse/context.h>
#include <stdbool.h>
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()`.
//
// The first `int` is the index to a function on the stack. If this is non-zero, that value will
// be copied to the thread's stack.
simple_callback_data* prepare_lua_callback(lua_State*, int);
// 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*);
// Simple implementation of `pa_context_success_cb_t` that calls a provided Lua function.
void success_callback(pa_context*, int, void*);

View file

@ -0,0 +1,305 @@
#include "context.h"
#include "lua_util.h"
#include "pulseaudio.h"
#include <pulse/context.h>
#include <pulse/error.h>
#include <pulse/subscribe.h>
/* Calls the user-provided callback with the updated state info.
*/
void context_state_callback(pa_context* c, void* userdata) {
simple_callback_data* data = (simple_callback_data*) userdata;
luaL_checktype(data->L, 1, LUA_TFUNCTION);
// `lua_call` will pop the function and arguments from the stack, but this callback will likely be called
// multiple times.
// To preseve the values for future calls, we need to duplicate them.
lua_pushvalue(data->L, 1);
// This can't really fail, but for consistency, we keep the error value.
lua_pushnil(data->L);
pa_context_state_t state = pa_context_get_state(c);
lua_pushinteger(data->L, state);
lua_call(data->L, 2, 0);
}
/* Calls the user-prodivded event callbacks.
*/
void context_event_callback(pa_context* c, pa_subscription_event_type_t event_type, uint32_t index, void* userdata) {
simple_callback_data* data = (simple_callback_data*) userdata;
lua_State* L = data->L;
luaL_checktype(L, 1, LUA_TTABLE);
luaL_checkudata(L, 2, LUA_PA_CONTEXT);
// Iterate over the list of subscription callbacks and call each one
lua_pushnil(L);
while (lua_next(L, 1) != 0) {
// TODO: Once we do have the "nothing here" value, we need to check for that here.
// Copy the `self` parameter
lua_pushvalue(L, 2);
lua_pushinteger(L, event_type);
lua_pushinteger(L, index);
lua_call(L, 3, 0);
}
}
void context_subscribe_success_callback(pa_context* _c, int success, void* userdata) {
simple_callback_data* data = (simple_callback_data*) userdata;
if (!success) {
luaL_error(data->L, "Failed to subscribe to events");
}
}
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);
if (ctx == NULL) {
return luaL_error(L, "failed to create pulseaudio 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");
}
lgi_ctx->context = ctx;
lgi_ctx->connected = FALSE;
lgi_ctx->subscribed = FALSE;
lgi_ctx->state_callback_data = prepare_lua_callback(L, 0);
lgi_ctx->event_callback_data = prepare_lua_callback(L, 0);
// Create the table used to store the subscription callbacks.
lua_newtable(lgi_ctx->event_callback_data->L);
luaL_getmetatable(L, LUA_PA_CONTEXT);
lua_setmetatable(L, -2);
// Copy the `context` value to the event callback
lua_pushvalue(L, -1);
lua_xmove(L, lgi_ctx->event_callback_data->L, 1);
return 1;
}
int context__gc(lua_State* L) {
lua_pa_context* ctx = luaL_checkudata(L, 1, LUA_PA_CONTEXT);
if (ctx->connected == TRUE) {
pa_context_disconnect(ctx->context);
ctx->connected = 0;
}
if (ctx->state_callback_data != NULL) {
free_lua_callback(ctx->state_callback_data);
}
if (ctx->event_callback_data != NULL) {
free_lua_callback(ctx->event_callback_data);
}
pa_context_unref(ctx->context);
return 0;
}
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;
if (luaL_getmetafield(L, 2, "__name") == LUA_TSTRING)
typearg = lua_tostring(L, -1);
else if (lua_type(L, 2) == LUA_TLIGHTUSERDATA)
typearg = "light userdata";
else
typearg = luaL_typename(L, 2);
return luaL_argerror(L, 2, lua_pushfstring(L, "string or nil expected, got %s", typearg));
}
luaL_checktype(L, 3, LUA_TFUNCTION);
lua_pa_context* ctx = luaL_checkudata(L, 1, LUA_PA_CONTEXT);
pa_context_flags_t flags = PA_CONTEXT_NOAUTOSPAWN;
if (nargs > 3)
flags = luaL_checkinteger(L, 4);
// Make sure the callback function is at a known position in the thread's stack
lua_settop(ctx->state_callback_data->L, 0);
lua_pushvalue(L, 3);
lua_xmove(L, ctx->state_callback_data->L, 1);
pa_context_set_state_callback(ctx->context, context_state_callback, ctx->state_callback_data);
pa_context_set_subscribe_callback(ctx->context, context_event_callback, ctx->event_callback_data);
// TODO: Check if I need to create bindings for `pa_spawn_api`.
int ret = pa_context_connect(ctx->context, server, flags, NULL);
if (ret < 0) {
return luaL_error(L, "failed to connect: %s", pa_strerror(ret));
}
return 0;
}
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, 3);
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, 3);
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;
}
int context_subscribe(lua_State* L) {
lua_pa_context* ctx = luaL_checkudata(L, 1, LUA_PA_CONTEXT);
luaL_checktype(L, 2, LUA_TFUNCTION);
// This call is only effective when the connection state is "ready".
// So we have to do it here, rather than during `context_connect`,
// but we also need to make sure it's only called once.
if (!ctx->subscribed) {
pa_context_subscribe(ctx->context,
PA_SUBSCRIPTION_MASK_ALL,
context_subscribe_success_callback,
ctx->event_callback_data);
}
size_t pos = lua_rawlen(ctx->event_callback_data->L, 1) + 1;
// Duplicate the callback function, so we can move it over to the other thread
// TODO: Do we actually need to duplicate?
lua_pushvalue(L, 2);
lua_xmove(L, ctx->event_callback_data->L, 1);
lua_rawseti(ctx->event_callback_data->L, 1, pos);
lua_pushinteger(L, pos);
return 1;
}
int context_unsubscribe(lua_State* L) {
lua_pa_context* ctx = luaL_checkudata(L, 1, LUA_PA_CONTEXT);
lua_State* thread_L = ctx->event_callback_data->L;
size_t pos = 0;
size_t len = lua_rawlen(thread_L, 1);
if (len == 0) {
return 0;
}
// TODO: Handle calling this twice on the same index.
// Given that we use an array to track things, and Lua's way of counting in arrays
// doesn't like `nil`s, we probably need a special "nothing here" value that's not `nil`,
// and signifies an index that has already been unsubscribed.
// TODO: Simplify things. Supporting just the index is enough.
// Comparing by function is convenient, but also confusing to inexperienced devs.
switch (lua_type(L, 2)) {
case LUA_TNUMBER: {
pos = lua_tointeger(L, 2);
break;
}
case LUA_TFUNCTION: {
bool found = false;
size_t i = 0;
// Duplicate the function value, so we can move it other to the other thread for comparing
lua_pushvalue(L, -1);
lua_xmove(L, thread_L, 1);
int fn_index = lua_gettop(thread_L);
lua_pushnil(L);
while (lua_next(thread_L, 1) != 0) {
++i;
if (lua_equal(thread_L, -1, fn_index) == 1) {
pos = i;
lua_pop(thread_L, 2);
break;
}
// Remove the value, but keep the key to continue iterating
lua_pop(thread_L, 1);
}
if (!found) {
return luaL_error(L, "couldn't find this function in the list of subscriptions");
}
break;
}
default: {
return luaL_argerror(L, 2, "expected number or function");
}
}
// TODO: As explained above, we need to handle calling unsubscribe twice better.
// Indices should not be re-used but replaced by a special "nothing here" value.
for (; pos < len; ++pos) {
lua_rawgeti(thread_L, 1, pos + 1);
lua_rawseti(thread_L, 1, pos);
}
lua_pushnil(thread_L);
lua_rawseti(thread_L, 1, len);
return 0;
}

View file

@ -0,0 +1,536 @@
/** 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 lua_libpulse_glib.context
*/
#pragma once
#include "callback.h"
#include <lauxlib.h>
#include <lua.h>
#include <pulse/context.h>
#include <pulse/mainloop-api.h>
#include <stdbool.h>
#define LUA_PA_CONTEXT "pulseaudio.context"
typedef struct lua_pa_context {
pa_context* context;
bool connected;
bool subscribed;
simple_callback_data* state_callback_data;
simple_callback_data* event_callback_data;
} lua_pa_context;
int context_new(lua_State*, pa_mainloop_api*);
int context__gc(lua_State*);
/// Callback Functions
/// @section callbacks
/** The callback signature for @{Context:subscribe}.
*
* See [here](https://freedesktop.org/software/pulseaudio/doxygen/subscribe.html) how to use the `event_type`
* parameter to determine the event that triggered the callback.
*
* @function event_callback
* @tparam Context context The context that the callback is subscribed to.
* @tparam number event_type A bitflag-ish value from libpulse that represents the event that caused the callback.
* @tparam number index
*/
/** The callback signature for @{Context:connect}.
*
* @function state_callback
* @tparam Context context The context that the callback is subscribed to.
* @tparam number state An enum that represents the current connection state.
*/
/// 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*);
/** Returns the current connection state.
*
* @function Context:get_state
* @treturn number
*/
int context_get_state(lua_State*);
/** Registers a callback function as event handler.
*
* This callback will be called whenever the server sends a suitable event.
*
* Any number of callbacks may be registered at the same time, and can be unscubscribed with
* @{Context:unsubscribe}, using the returned subscription ID.
*
* This must called after the connection state changed to "ready" (index `4`). Otherwise
* it will have no effect.
*
* @function Context:subscribe
* @tparam function cb
* @treturn number The subscription ID.
*/
int context_subscribe(lua_State*);
/** Removes an event handler subscription.
*
* Subscriptions may be removed either by their ID or by their callback function.
*
* @function Context:unsubscribe
* @tparam number|function handler The handler to remove.
*/
int context_unsubscribe(lua_State*);
/** Sets the default sink.
*
* @function Context:set_default_sink
* @async
* @tparam string sink Name of the sink to set as default.
* @treturn[opt] string error
* @treturn boolean
*/
int context_set_default_sink(lua_State*);
/** Sets the default source.
*
* @function Context:set_default_source
* @async
* @tparam string source Name of the source to set as default.
* @treturn[opt] string error
* @treturn boolean
*/
int context_set_default_source(lua_State*);
/** 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},
{ NULL, NULL }
};
static const struct luaL_Reg context_f[] = {
{"connect", context_connect },
{ "disconnect", context_disconnect },
{ "subscribe", context_subscribe },
{ "unsubscribe", context_unsubscribe },
{ "get_state", context_get_state },
{ "set_default_sink", context_set_default_sink },
{ "set_default_source", context_set_default_source },
{ "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 }
};

View file

@ -0,0 +1,532 @@
#include "convert.h"
#include "proplist.h"
#include "volume.h"
#include <pulse/xmalloc.h>
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);
}

View file

@ -0,0 +1,20 @@
#pragma once
#include <lua.h>
#include <pulse/error.h>
#include <pulse/introspect.h>
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*);

View file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,22 @@
#pragma once
#include <lauxlib.h>
#include <lua.h>
#define LUA_MOD_EXPORT extern
#if LUA_VERSION_NUM <= 501
#define lua_rawlen lua_objlen
#define luaL_newlib(L, l) (luaL_newlibtable(L, l), luaL_setfuncs(L, l, 0))
#define luaL_newlibtable(L, l) (lua_createtable(L, 0, sizeof(l) / sizeof(l[0])))
#endif
#if LUA_VERSION_NUM > 501
#define lua_equal(L, i1, i2) lua_compare(L, i1, i2, LUA_OPEQ)
#endif
typedef struct luaU_enumfield {
const char* name;
const char* value;
} luaU_enumfield;

View file

@ -0,0 +1,214 @@
#include "proplist.h"
#include <pulse/xmalloc.h>
// 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;
}

View file

@ -0,0 +1,318 @@
/** 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 lua_libpulse_glib.proplist
*/
#pragma once
#include "lua_util.h"
#include <lauxlib.h>
#include <lua.h>
#include <pulse/proplist.h>
#include <stdbool.h>
#define LUA_PA_PROPLIST "lua_libpulse_glib.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 },
#if PA_CHECK_VERSION(15, 0, 0)
{ "CONTEXT_FORCE_DISABLE_SHM", PA_PROP_CONTEXT_FORCE_DISABLE_SHM },
{ "BLUETOOTH_CODEC", PA_PROP_BLUETOOTH_CODEC },
#endif
{ NULL, NULL }
};

View file

@ -0,0 +1,166 @@
#include "pulseaudio.h"
#include "context.h"
#include "lua_util.h"
#include "proplist.h"
#include "volume.h"
#include <lauxlib.h>
#include <lua.h>
#include <lualib.h>
#include <pulse/glib-mainloop.h>
#if LUA_VERSION_NUM <= 501
// Shamelessly copied from Lua 5.3 source.
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? */
lua_pushboolean(L, 0);
else {
int i;
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_setfield(L, -(nup + 2), l->name);
}
lua_pop(L, nup); /* remove upvalues */
}
#endif
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?");
lua_error(L);
return 0;
}
pulseaudio* pa = lua_newuserdata(L, sizeof(pulseaudio));
if (!pa) {
return luaL_error(L, "failed to create pulseaudio userdata");
}
luaL_getmetatable(L, LUA_PULSEAUDIO);
lua_setmetatable(L, -2);
pa->mainloop = pa_glib_mainloop_new(ctx);
return 1;
}
/*
* Proxies table index operations to our metatable.
*/
int pulseaudio__index(lua_State* L) {
const char* index = luaL_checkstring(L, 2);
luaL_getmetatable(L, LUA_PULSEAUDIO);
lua_getfield(L, -1, index);
return 1;
}
/*
* Free the PulseAudio object
*/
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) {
pulseaudio* pa = luaL_checkudata(L, 1, LUA_PULSEAUDIO);
return context_new(L, pa_glib_mainloop_get_api(pa->mainloop));
}
void createlib_volume(lua_State* L) {
luaL_newmetatable(L, LUA_PA_VOLUME);
luaL_newlib(L, volume_f);
lua_setfield(L, -2, "__index");
luaL_setfuncs(L, volume_mt, 0);
luaL_newlib(L, volume_lib);
lua_setmetatable(L, -2);
}
void createlib_proplist(lua_State* L) {
luaL_newmetatable(L, LUA_PA_PROPLIST);
luaL_newlib(L, proplist_f);
lua_setfield(L, -2, "__index");
luaL_setfuncs(L, proplist_mt, 0);
luaL_newlib(L, proplist_lib);
// 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);
luaL_newlib(L, context_f);
lua_setfield(L, -2, "__index");
luaL_setfuncs(L, context_mt, 0);
}
void createlib_pulseaudio(lua_State* L) {
luaL_newmetatable(L, LUA_PULSEAUDIO);
luaL_newlib(L, pulseaudio_f);
lua_setfield(L, -2, "__index");
luaL_setfuncs(L, pulseaudio_mt, 0);
luaL_newlib(L, pulseaudio_lib);
}
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);
lua_pushstring(L, LUA_PA_REGISTRY);
lua_newtable(L);
lua_settable(L, -3);
lua_rawset(L, LUA_REGISTRYINDEX);
createlib_context(L);
createlib_proplist(L);
createlib_pulseaudio(L);
return 1;
}
LUA_MOD_EXPORT int luaopen_lua_libpulse_glib_volume(lua_State* L) {
luaL_newmetatable(L, LUA_PA_VOLUME);
luaL_newlib(L, volume_f);
lua_setfield(L, -2, "__index");
luaL_setfuncs(L, volume_mt, 0);
luaL_newlib(L, volume_lib);
return 1;
}

View file

@ -0,0 +1,60 @@
/** Bindings for PulseAudio's libpulse, using the GLib Main Loop.
*
* @module lua_libpulse_glib
*/
#pragma once
#include "lauxlib.h"
#include "lua.h"
#include <pulse/glib-mainloop.h>
#define LUA_PULSEAUDIO "lua_libpulse_glib"
#define LUA_PA_REGISTRY "lua_libpulse_glib.registry"
typedef struct pulseaudio {
pa_glib_mainloop* mainloop;
} pulseaudio;
/** Creates a new PulseAudio object.
*
* @function new
* @return[type=PulseAudio]
*/
int pulseaudio_new(lua_State*);
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 }
};
static const struct luaL_Reg pulseaudio_lib[] = {
{"new", pulseaudio_new},
{ NULL, NULL }
};

View file

@ -0,0 +1,343 @@
#include "volume.h"
#include "lua_util.h"
#include <pulse/xmalloc.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
int volume_new(lua_State* L) {
pa_cvolume* cvol = volume_from_lua(L, 1);
return volume_to_lua(L, cvol);
}
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, index)) {
case LUA_TTABLE: {
uint8_t channels = (uint8_t) lua_rawlen(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 = (int) luaL_checkinteger(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");
}
}
}
int volume_from_dB(lua_State* L) {
double value = (double) luaL_checknumber(L, 1);
lua_pushinteger(L, pa_sw_volume_from_dB(value));
return 1;
}
int volume_to_dB(lua_State* L) {
pa_volume_t value = luaL_checkinteger(L, 1);
lua_pushnumber(L, pa_sw_volume_to_dB(value));
return 1;
}
int volume_from_linear(lua_State* L) {
double value = (double) luaL_checknumber(L, 1);
lua_pushinteger(L, pa_sw_volume_from_linear(value));
return 1;
}
int volume_to_linear(lua_State* L) {
pa_volume_t value = luaL_checkinteger(L, 1);
lua_pushnumber(L, pa_sw_volume_to_linear(value));
return 1;
}

View file

@ -0,0 +1,296 @@
/** 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 lua_libpulse_glib.volume
*/
#ifndef volume_h_INCLUDED
#define volume_h_INCLUDED
#include <lauxlib.h>
#include <lua.h>
#include <pulse/volume.h>
#define LUA_PA_VOLUME "lua_libpulse_glib.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
/** Creates an instance of @{Volume}.
*
* @function new
* @tparam table values An array of channel volumes.
* @treturn Volume
*/
int volume_new(lua_State*);
/** 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*);
/** Converts a decibel value to an integer volume value.
*
* This is only valid for software volumes. It does not operate
* on instances of @{Volume}.
*
* @function from_dB
* @tparam number value
* @treturn number
*/
int volume_from_dB(lua_State*);
/** Converts an integer volume value to a decibel value.
*
* This is only valid for software volumes. It does not operate
* on instances of @{Volume}.
*
* @function to_dB
* @tparam number value
* @treturn number
*/
int volume_to_dB(lua_State*);
/** Converts a linear factor to an integer volume value.
*
* `0.0` and less is muted, `1.0` is normal volume.
*
* This is only valid for software volumes. It does not operate
* on instances of @{Volume}.
*
* @function from_linear
* @tparam number value
* @treturn number
*/
int volume_from_linear(lua_State*);
/** Converts an integer volume value to linear factor.
*
* This is only valid for software volumes. It does not operate
* on instances of @{Volume}.
*
* @function to_linear
* @tparam number value
* @treturn number
*/
int volume_to_linear(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[] = {
{"new", volume_new },
{ "from_dB", volume_from_dB },
{ "to_dB", volume_to_dB },
{ "from_linear", volume_from_linear},
{ "to_linear", volume_to_linear },
{ "is_valid", volume_is_valid },
{ NULL, NULL }
};
#endif // volume_h_INCLUDED

View file

@ -1,8 +0,0 @@
local lgi = require("lgi")
local pulseaudio = require("lgi_pulseaudio")
local loop = lgi.GLib.MainLoop.new()
print(pulseaudio.new)
print(loop:get_context())
-- print(pulseaudio.new(loop:get_context()))

View file

@ -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