1
Fork 0

Compare commits

..

43 commits

Author SHA1 Message Date
aca076f51b
Implement event subscription 2023-11-01 22:09:12 +01:00
d2f424cbe2
Improve running tests 2023-11-01 22:08:06 +01:00
30db8863e3
Fix module initialization
The way C modules are initialized was affected by one of the major
breaking changes between 5.x versions.
Rather than trying to cater to each version individually, we now
just backport from newer versions. Namely from 5.2 to 5.1.
2023-11-01 20:24:06 +01:00
3838b9f52a
bug(ci): Fix passing variables to task 2022-08-18 13:31:49 +02:00
c8bf8931c8
ci: Build and deploy documentation 2022-08-17 15:13:56 +02:00
45a1c9b711
ci: Only build the library for testing
The docs don't need to be build for that.
2022-08-17 15:07:59 +02:00
7e96f344be
chore: Remove unused Bors config 2022-08-14 17:37:57 +02:00
b46efff622
doc: Fix CI badge 2022-08-14 17:37:17 +02:00
2152186e23
ci: Switch to master branch 2022-08-14 17:35:51 +02:00
ee86906dd4
chore: Remove unused files 2022-08-14 17:32:04 +02:00
b9cda2deba
feat(ci): Allow CI to fail for 5.4
LGI doesn't have a rock compatible with 5.4, yet.
2022-08-14 17:19:29 +02:00
7ea226fa51
feat: Implement Concourse CI pipeline 2022-08-14 16:58:00 +02:00
1745e5d4ad
feat(volume): Add conversion functions 2022-08-11 13:37:00 +02:00
3173460b17
bug(volume): Fix constructor documentation 2022-08-10 23:47:16 +02:00
862a265a70
bug: Fix version incompatibility
Even between minor versions, Lua simply removes or renames functions.
2022-08-10 13:57:15 +02:00
d10b61eaf9
build: Add target to uninstall 2022-08-10 13:56:58 +02:00
8fa6f47cfe
build: Only rebuild docs on changes 2022-08-10 13:56:24 +02:00
243c50f921
chore: Ignore SASS cache 2022-08-10 13:55:48 +02:00
5a981b8212
bug: Fix installing 2022-08-10 13:07:34 +02:00
5b3559bf0e
doc: Add disclaimer about current project state 2022-06-22 15:33:02 +02:00
9e9ac22e82
fix(context): Fix setting volume
The volume object needs to be kept in memory until the operation has
finished.
2022-05-23 16:32:04 +02:00
3ad2c27ff8
feat(volume): Add constructor 2022-05-23 16:28:06 +02:00
b1b8988e7e
fix(volume): Fix volume library
This also makes it possible to load it independently.
2022-05-23 16:27:10 +02:00
0203cbce4c
fix: Fix library names 2022-05-23 15:42:24 +02:00
6eb79d4d8d
fix(volume): Fix hard coded stack index 2022-05-23 15:38:57 +02:00
e0aaa99cb1
fix(context): Fix callback functions
The stack setup for callback threads was all over the place, and pretty
messed up.
2022-05-23 14:52:55 +02:00
188c8a7666
doc: Fix missing function parameter in example 2022-05-20 19:50:24 +02:00
5634e205a2
fix(context): Fix missing methods
Some methods weren't actually registered in the class' metatable.
2022-05-16 16:41:40 +02:00
eed57afbda
feat(context): Simplify callbacks
The same simple callback structure works for all the callbacks I've
built so far.
2022-05-16 16:24:36 +02:00
bfc322c3ad
feat(context): Implement event subscriptions 2022-05-16 16:01:51 +02:00
d88abd87c6
feat(context): Add getter for connection state 2022-05-16 14:22:56 +02:00
4e1b02e65f
feat(context): Implement default sink and source setters 2022-05-16 13:53:20 +02:00
65483ec8a5
doc: Fix code block 2022-05-16 13:45:19 +02:00
533f9a563d
build: Remove unused dependencies 2022-05-15 23:17:36 +02:00
69c2fd7faf
doc: Improve quick start documentation 2022-05-15 23:17:21 +02:00
12dd08f13a
doc: Fix module names 2022-05-15 23:12:13 +02:00
d303480640
build: Fix rockspec summary 2022-05-15 23:11:50 +02:00
9a780045ff
doc: Add main usage example 2022-05-15 23:11:28 +02:00
00d95de6d9
chore: Ignore LuaRocks working files 2022-05-15 23:11:00 +02:00
366e08498e
feat: Implement introspection for sinks and sources
This provides a way to query the current state for the primary
data structures, as well as common setters.
2022-05-15 22:56:55 +02:00
f8b2a45740
chore: Improve make recipe echoing
Replaces GNU Make's default output, which simply dumps the whole
whole command line, with a custom echo that skips most flags and uses
bold text to distinguish it from command output.
2022-04-22 10:35:13 +02:00
c47054e66e
chore: Configure tooling 2022-04-21 14:04:18 +02:00
26c093ec27
build: Fix color output in Makefile 2022-04-20 18:00:08 +02:00
42 changed files with 4385 additions and 1192 deletions

View file

@ -1,7 +1,6 @@
return { return {
default = { default = {
verbose = true, verbose = true,
helper = "./spec/_helper.lua",
lpath = "./src/?.lua;./src/?/init.lua;./src/?/?.lua;./tests/?.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,190 +0,0 @@
b pulseaudio__index
b pulseaudio_new_context
r test.lua
r test.lua
b pulseaudio_new_context
b pulseaudio__index
r test.lua
c
c
info lua5.1
help info
info program
info target
show debug-file-directory
r test.lua
r test.lua
help
help running
target exec
target exec ~/build/lua-5.1.5/src/lua
target exec /home/gerlui/build/lua-5.1.5/src/lua
help readelf
help
apropos symbol
symbol-file /home/gerlui/build/lua-5.1.5/src/lua
b pulseaudio__index
r test.lua
symbol-file /home/gerlui/build/lua-5.1.5/src/lua
b pulseaudio__index
r test.lua
b pulseaudio__index
b pulseaudio_new_context
r test.lua
source ~/.config/gdb/lua-gdb.py
dashboard
apropos arg
symbol-file lua.debug
help symbol-file
r
bpulseaudio_new_context
b pulseaudio_new_context
r
r
b pulseaudio__index
r
b pulseaudio_new_context
b pulseaudio__index
r test.lua
b lua_error
r
b longjmp
b lonjmp
luastack
dashboard
help show
apropos variable
info variables
info tvariables
info locals
info scope
info scope function
info line
info scope line
tui
tui enable
tui disable
dhasboard
dashboard
apropos arg
explore L
dashboard
luastack
luastack :
luastack L
luatraceback
luagetlocal
luagetlocal L
r
r
b pulseaudio__index
b pulseaudio_new_context
r
b lua_error
r
show directories
b pulseaudio__index
b pulseaudio_new_context
b lua_error
r
b pulseaudio_new_context
r
r test.lua
b pulseaudio_new_context
b pulseaudio__index
b lua_error
r
b pulseaudio__index
b pulseaudio_new_context
r
b pulseaudio_new_context
r
b pulseaudio_new_context
r test.lua
b pulseaudio_new_context
r
show directories
show directories
b pulseaudio_new_context
r
b pulseaudio_new_context
r
show directories
b pulseaudio_new_context
r
help
help running
dashboard
s
help running
s
help running
rs
b context_new
help c
c
r
n
n
b context_connect
r test.lua
s
s
s
q
b context_connect
run test.lua
symbol-file lua.debug
s
s
s
s
s
n
n
n
n
n
n
n
s
s
n
n
n
n
n
n
n
n
n
r test.lua
n
n
n
n
n
q
b context__gc
r test.lua
n
help running
dashboard
s
q
b context__gc
r
n
q
b context__gc
r
n
q
symbol-file lua.debug
b context_connect
r
b context_connect
r test.lua
b context_get_server_info
r
q

View file

@ -1,5 +0,0 @@
symbol-file -readnow lua.debug
# source ~/.config/gdb/lua-gdb.py
set directories src/lua_libpulse_glib:/home/gerlui/build/lua-5.1.5/src
# set directories src/lua_libpulse_glib
set args test.lua

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

11
.gitignore vendored
View file

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

View file

@ -1,21 +0,0 @@
{
"configurations": [
{
"name": "Linux",
"includePath": [
"${workspaceFolder}/src/lua_libpulse_glib/**",
"/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

@ -43,6 +43,8 @@ OBJS = $(shell find src -type f -iname '*.c' | sed 's/\(.*\)\.c$$/$(BUILD_DIR)\/
TARGET = $(BUILD_DIR)/$(PROJECT).so TARGET = $(BUILD_DIR)/$(PROJECT).so
LUA_CPATH = $(shell echo "$${PWD}/$(BUILD_DIR)/?.so;$${LUA_CPATH}")
ifdef CI ifdef CI
CHECK_ARGS ?= --formatter TAP CHECK_ARGS ?= --formatter TAP
TEST_ARGS ?= --output=TAP TEST_ARGS ?= --output=TAP
@ -50,55 +52,67 @@ TEST_ARGS ?= --output=TAP
CCFLAGS += -Werror CCFLAGS += -Werror
endif 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)
.PHONY: all clean doc doc-content doc-styles install uninstall test check rock
all: build doc
build: $(TARGET) build: $(TARGET)
$(BUILD_DIR)/%.o: %.c $(BUILD_DIR)/%.o: %.c
@mkdir -p $(shell dirname "$@") @mkdir -p $(shell dirname "$@")
$(CC) -c $(CCFLAGS) $< -o $@ @echo "$(title)$(CC) $< -o $@$(reset)"
@$(CC) -c $(CCFLAGS) $< -o $@
$(TARGET): $(OBJS) $(TARGET): $(OBJS)
$(CC) $(LIBFLAG) -o $@ $(OBJS) $(LIBS) @echo "$(title)$(CC) -o $@$(reset)"
@$(CC) $(LIBFLAG) -o $@ $(OBJS) $(LIBS)
doc-styles: $(BUILD_DIR)/doc/index.html:
@printf "\e[1;97mGenerate stylesheet\e[0m\n"
sass doc/ldoc.scss $(BUILD_DIR)/doc/ldoc.css
doc-content:
@mkdir -p "$(BUILD_DIR)/doc" "$(BUILD_DIR)/src" @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)" 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" ldoc --config=doc/config.ld --dir "$(BUILD_DIR)/doc" --project $(PROJECT) "$(BUILD_DIR)/src"
doc: doc-content doc-styles $(BUILD_DIR)/doc/ldoc.css: doc/ldoc.scss
ifdef CI @mkdir -p "$(BUILD_DIR)/doc"
touch "$(BUILD_DIR)/doc/.nojekyll" @echo "$(title)Generate stylesheet$(reset)"
endif 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: clean:
rm -r out/ rm -r out/
install: build doc install: build doc
@printf "\e[1;97mInstall C library\e[0m\n" @echo "$(title)Install C library\033[0m"
xargs install -vDm 644 -t $(INSTALL_LIBDIR)/$(PROJECT) $(TARGET) install -vDm 644 -t $(INSTALL_LIBDIR) $(TARGET)
@printf "\e[1;97mInstall documentation\e[0m\n" @echo "$(title)Install documentation\033[0m"
install -vd $(INSTALL_DOCDIR) install -vd $(INSTALL_DOCDIR)
cp -vr $(BUILD_DIR)/doc/* $(INSTALL_DOCDIR) cp -vr $(BUILD_DIR)/doc/* $(INSTALL_DOCDIR)
uninstall:
rm $(INSTALL_LIBDIR)/$(PROJECT).so
rm -r $(INSTALL_DOCDIR)
check: check:
@echo "Nothing to do" @echo "Nothing to do"
test: spec: build
busted --config-file=.busted.lua --lua=$(LUA) $(TEST_ARGS) busted --config-file=.busted.lua --lua=$(LUA) $(TEST_ARGS)
test: build
$(LUA) test.lua
rock: rock:
luarocks --local --lua-version $(LUA_VERSION) make rocks/lua-libpulse-glib-scm-1.rockspec luarocks --local --lua-version $(LUA_VERSION) make rocks/lua-libpulse-glib-scm-1.rockspec
run: build
env LUA_CPATH="./out/?.so;${LUA_CPATH}" $(LUA) test.lua
debug: build
env LUA_CPATH="./out/?.so;${LUA_CPATH}" gdb $(LUA)

View file

@ -5,7 +5,6 @@ ifdef::env-github,env-browser[]
:toc: macro :toc: macro
:toclevels: 1 :toclevels: 1
endif::[] endif::[]
ifdef::env-github[]
:branch: master :branch: master
:status: :status:
:outfilesuffix: .adoc :outfilesuffix: .adoc
@ -15,31 +14,56 @@ ifdef::env-github[]
:note-caption: :paperclip: :note-caption: :paperclip:
:tip-caption: :bulb: :tip-caption: :bulb:
:warning-caption: :warning: :warning-caption: :warning:
endif::[] :url-ci: https://ci.sclu1034.dev/teams/main/pipelines/lua-libpulse-glib
:url-ci-github: https://github.com/sclu1034/lua-libpulse-glib/actions :url-ci-badge: https://ci.sclu1034.dev/api/v1/teams/main/pipelines/lua-libpulse-glib/badge
:url-ci-badge-github: https://img.shields.io/github/workflow/status/sclu1034/lua-libpulse-glib/Lint%20&%20Test?style=flat-square
:url-license-badge: https://img.shields.io/badge/license-GPLv3-brightgreen?style=flat-square :url-license-badge: https://img.shields.io/badge/license-GPLv3-brightgreen?style=flat-square
:url-luarocks-badge: https://img.shields.io/luarocks/v/sclu1034/lua-libpulse-glib?style=flat-square :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 :url-luarocks-link: https://luarocks.org/modules/sclu1034/lua-libpulse-glib
image:{url-license-badge}[License] image:{url-license-badge}[License]
ifdef::status[] 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::[] endif::[]
image:{url-luarocks-badge}[LuaRocks Package, link={url-luarocks-link}] 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://freedesktop.org/software/pulseaudio/doxygen/index.html[libpulse] bindings for use with a GLib MainLoop via
https://github.com/lgi-devs/lgi/[LGI]. 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.
_lua-libpulse-glib_ is available via LuaRocks: == Quick Start
Install https://github.com/lgi-devs/lgi[lgi] and _lua_libpulse_glib_ from LuaRocks:
[source,shell] [source,shell]
---- ----
luarocks install lua-libpulse-glib 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,90 +0,0 @@
[
{
"arguments": [
"/usr/bin/x86_64-linux-gnu-gcc-10",
"-c",
"-fPIC",
"-Wall",
"-g",
"-D_REENTRANT",
"-I/usr/include/gobject-introspection-1.0",
"-I/usr/include/glib-2.0",
"-I/usr/lib/x86_64-linux-gnu/glib-2.0/include",
"-I/usr/include/lua5.1",
"-I/usr/include/lua5.1",
"-I./",
"src/lua_libpulse_glib/context.c",
"-o",
"out/src/lua_libpulse_glib/context.o"
],
"directory": "/home/gerlui/projects/lgi-pulseaudio",
"file": "/home/gerlui/projects/lgi-pulseaudio/src/lua_libpulse_glib/context.c",
"output": "/home/gerlui/projects/lgi-pulseaudio/out/src/lua_libpulse_glib/context.o"
},
{
"arguments": [
"/usr/bin/x86_64-linux-gnu-gcc-10",
"-c",
"-fPIC",
"-Wall",
"-g",
"-D_REENTRANT",
"-I/usr/include/gobject-introspection-1.0",
"-I/usr/include/glib-2.0",
"-I/usr/lib/x86_64-linux-gnu/glib-2.0/include",
"-I/usr/include/lua5.1",
"-I/usr/include/lua5.1",
"-I./",
"src/lua_libpulse_glib/pulseaudio.c",
"-o",
"out/src/lua_libpulse_glib/pulseaudio.o"
],
"directory": "/home/gerlui/projects/lgi-pulseaudio",
"file": "/home/gerlui/projects/lgi-pulseaudio/src/lua_libpulse_glib/pulseaudio.c",
"output": "/home/gerlui/projects/lgi-pulseaudio/out/src/lua_libpulse_glib/pulseaudio.o"
},
{
"arguments": [
"/usr/bin/x86_64-linux-gnu-gcc-10",
"-c",
"-fPIC",
"-Wall",
"-g",
"-D_REENTRANT",
"-I/usr/include/gobject-introspection-1.0",
"-I/usr/include/glib-2.0",
"-I/usr/lib/x86_64-linux-gnu/glib-2.0/include",
"-I/usr/include/lua5.1",
"-I/usr/include/lua5.1",
"-I./",
"src/lua_libpulse_glib/init.c",
"-o",
"out/src/lua_libpulse_glib/init.o"
],
"directory": "/home/gerlui/projects/lgi-pulseaudio",
"file": "/home/gerlui/projects/lgi-pulseaudio/src/lua_libpulse_glib/init.c",
"output": "/home/gerlui/projects/lgi-pulseaudio/out/src/lua_libpulse_glib/init.o"
},
{
"arguments": [
"/usr/bin/x86_64-linux-gnu-gcc-10",
"-c",
"-fPIC",
"-Wall",
"-g",
"-D_REENTRANT",
"-I/usr/include/gobject-introspection-1.0",
"-I/usr/include/glib-2.0",
"-I/usr/lib/x86_64-linux-gnu/glib-2.0/include",
"-I/usr/include/lua5.1",
"-I/usr/include/lua5.1",
"-I./",
"src/lua_libpulse_glib/introspection.c",
"-o",
"out/src/lua_libpulse_glib/introspection.o"
],
"directory": "/home/gerlui/projects/lgi-pulseaudio",
"file": "/home/gerlui/projects/lgi-pulseaudio/src/lua_libpulse_glib/introspection.c",
"output": "/home/gerlui/projects/lgi-pulseaudio/out/src/lua_libpulse_glib/introspection.o"
}
]

View file

@ -1,13 +1,49 @@
project = 'lgi-async-extra' project = 'lua-libpulse-glib'
title = 'lgi-async-extra' title = 'lua-libpulse-glib'
description = 'An asynchronous high(er)-level API wrapper for lua-lgi' 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 template = true
format = 'discount' format = 'discount'
pretty = 'lua' pretty = 'lua'
prettify_files = 'show' -- prettify_files = 'show'
backtick_references = false backtick_references = false
wrap = true wrap = true
no_space_before_args = true no_space_before_args = true

BIN
lua.debug

Binary file not shown.

View file

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

0
spec/.gitkeep Normal file
View file

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

@ -1,20 +1,24 @@
#include "pulseaudio.h"
#include "context.h" #include "context.h"
#include "pulse/context.h"
#include "lua_util.h"
#include "pulseaudio.h"
#include <pulse/context.h>
#include <pulse/error.h>
#include <pulse/subscribe.h>
void /* Calls the user-provided callback with the updated state info.
context_state_callback(pa_context* c, void* userdata) */
{ void context_state_callback(pa_context* c, void* userdata) {
context_state_callback_data* data = (context_state_callback_data*) userdata; simple_callback_data* data = (simple_callback_data*) userdata;
luaL_checktype(data->L, 1, LUA_TFUNCTION); luaL_checktype(data->L, 1, LUA_TFUNCTION);
luaL_checkudata(data->L, 2, LUA_PA_CONTEXT);
// `lua_call` will pop the function and arguments from the stack, but this callback will likely be called // `lua_call` will pop the function and arguments from the stack, but this callback will likely be called
// multiple times. // multiple times.
// To preseve the values for future calls, we need to duplicate them. // To preseve the values for future calls, we need to duplicate them.
lua_pushvalue(data->L, 1); lua_pushvalue(data->L, 1);
lua_pushvalue(data->L, 2); // 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); pa_context_state_t state = pa_context_get_state(c);
lua_pushinteger(data->L, state); lua_pushinteger(data->L, state);
@ -23,9 +27,39 @@ context_state_callback(pa_context* c, void* userdata)
} }
int /* Calls the user-prodivded event callbacks.
context_new(lua_State* L, pa_mainloop_api* pa_api) */
{ 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); const char* name = luaL_checkstring(L, -1);
// TODO: libpulse recommends using `new_with_proplist` instead. But I need to figure out that `proplist` first. // 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); pa_context* ctx = pa_context_new(pa_api, name);
@ -33,34 +67,31 @@ context_new(lua_State* L, pa_mainloop_api* pa_api)
return luaL_error(L, "failed to create pulseaudio context"); return luaL_error(L, "failed to create pulseaudio context");
} }
lua_pa_context* lgi_ctx = lua_newuserdata (L, sizeof(lua_pa_context)); lua_pa_context* lgi_ctx = lua_newuserdata(L, sizeof(lua_pa_context));
if (lgi_ctx == NULL) { if (lgi_ctx == NULL) {
return luaL_error(L, "failed to create context userdata"); return luaL_error(L, "failed to create context userdata");
} }
lgi_ctx->context = ctx; lgi_ctx->context = ctx;
lgi_ctx->connected = FALSE; lgi_ctx->connected = FALSE;
lgi_ctx->state_callback_data = (context_state_callback_data*) calloc(1, sizeof(struct context_state_callback_data)); 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); luaL_getmetatable(L, LUA_PA_CONTEXT);
lua_setmetatable(L, -2); 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; return 1;
} }
int int context__gc(lua_State* L) {
context__index(lua_State* L)
{
const char* index = luaL_checkstring(L, 2);
luaL_getmetatable(L, LUA_PA_CONTEXT);
lua_getfield(L, -1, index);
return 1;
}
int
context__gc(lua_State* L)
{
lua_pa_context* ctx = luaL_checkudata(L, 1, LUA_PA_CONTEXT); lua_pa_context* ctx = luaL_checkudata(L, 1, LUA_PA_CONTEXT);
if (ctx->connected == TRUE) { if (ctx->connected == TRUE) {
@ -69,13 +100,11 @@ context__gc(lua_State* L)
} }
if (ctx->state_callback_data != NULL) { if (ctx->state_callback_data != NULL) {
lua_pushstring(L, LUA_PULSEAUDIO); free_lua_callback(ctx->state_callback_data);
lua_rawget(L, LUA_REGISTRYINDEX); }
lua_pushstring(L, LUA_PA_REGISTRY); if (ctx->event_callback_data != NULL) {
lua_gettable(L, -2); free_lua_callback(ctx->event_callback_data);
luaL_unref(L, -1, ctx->state_callback_data->thread_ref);
free(ctx->state_callback_data);
} }
pa_context_unref(ctx->context); pa_context_unref(ctx->context);
@ -83,16 +112,14 @@ context__gc(lua_State* L)
} }
int int context_connect(lua_State* L) {
context_connect(lua_State* L)
{
int nargs = lua_gettop(L); int nargs = lua_gettop(L);
const char* server = NULL; const char* server = NULL;
if (lua_type(L, 2) == LUA_TSTRING) if (lua_type(L, 2) == LUA_TSTRING)
server = lua_tostring(L, 2); server = lua_tostring(L, 2);
else if (lua_type(L, 2) != LUA_TNIL) { else if (lua_type(L, 2) != LUA_TNIL) {
const char *typearg; const char* typearg;
if (luaL_getmetafield(L, 2, "__name") == LUA_TSTRING) if (luaL_getmetafield(L, 2, "__name") == LUA_TSTRING)
typearg = lua_tostring(L, -1); typearg = lua_tostring(L, -1);
else if (lua_type(L, 2) == LUA_TLIGHTUSERDATA) else if (lua_type(L, 2) == LUA_TLIGHTUSERDATA)
@ -111,25 +138,13 @@ context_connect(lua_State* L)
if (nargs > 3) if (nargs > 3)
flags = luaL_checkinteger(L, 4); flags = luaL_checkinteger(L, 4);
// Prepare a new thread to run the callback with // Make sure the callback function is at a known position in the thread's stack
lua_pushstring(L, LUA_PULSEAUDIO); lua_settop(ctx->state_callback_data->L, 0);
lua_rawget(L, LUA_REGISTRYINDEX);
lua_pushstring(L, LUA_PA_REGISTRY);
lua_gettable(L, -2);
lua_State* thread = lua_newthread(L);
int thread_ref = luaL_ref(L, -2);
// Copy the callback function and arguments to the thread's stack
lua_pushvalue(L, 3); lua_pushvalue(L, 3);
lua_pushvalue(L, 1); lua_xmove(L, ctx->state_callback_data->L, 1);
lua_xmove(L, thread, 2);
context_state_callback_data* data = calloc(1, sizeof(struct context_state_callback_data)); pa_context_set_state_callback(ctx->context, context_state_callback, ctx->state_callback_data);
data->L = thread; pa_context_set_subscribe_callback(ctx->context, context_event_callback, ctx->event_callback_data);
data->thread_ref = thread_ref;
ctx->state_callback_data = data;
pa_context_set_state_callback(ctx->context, context_state_callback, data);
// TODO: Check if I need to create bindings for `pa_spawn_api`. // TODO: Check if I need to create bindings for `pa_spawn_api`.
int ret = pa_context_connect(ctx->context, server, flags, NULL); int ret = pa_context_connect(ctx->context, server, flags, NULL);
@ -141,11 +156,150 @@ context_connect(lua_State* L)
} }
int int context_disconnect(lua_State* L) {
context_get_state(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); lua_pa_context* ctx = luaL_checkudata(L, 1, LUA_PA_CONTEXT);
pa_context_state_t state = pa_context_get_state(ctx->context); pa_context_state_t state = pa_context_get_state(ctx->context);
lua_pushinteger(L, state); lua_pushinteger(L, state);
return 1; 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

@ -1,44 +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 #pragma once
#include "stdbool.h" #include "callback.h"
#include "lua.h"
#include "lauxlib.h" #include <lauxlib.h>
#include "pulse/context.h" #include <lua.h>
#include "pulse/mainloop-api.h" #include <pulse/context.h>
#include "introspection.h" #include <pulse/mainloop-api.h>
#include <stdbool.h>
#define LUA_PA_CONTEXT "pulseaudio.context" #define LUA_PA_CONTEXT "pulseaudio.context"
typedef struct context_state_callback_data {
lua_State* L;
int thread_ref;
} context_state_callback_data;
typedef struct lua_pa_context { typedef struct lua_pa_context {
pa_context* context; pa_context* context;
bool connected; bool connected;
context_state_callback_data* state_callback_data; bool subscribed;
simple_callback_data* state_callback_data;
simple_callback_data* event_callback_data;
} lua_pa_context; } lua_pa_context;
int int context_new(lua_State*, pa_mainloop_api*);
context_new(lua_State*, pa_mainloop_api*); int context__gc(lua_State*);
int
context__index(lua_State*); /// Callback Functions
int /// @section callbacks
context__gc(lua_State*);
int /** The callback signature for @{Context:subscribe}.
context_connect(lua_State*); *
* 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.
*/
static const struct luaL_Reg context_mt [] = { /// Context
{"__index", context__index}, /// @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}, {"__gc", context__gc},
{"connect", context_connect}, { NULL, NULL }
{"get_server_info", context_get_server_info}, };
{"get_sinks", context_get_sink_info_list},
{"get_sources", context_get_source_info_list},
{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*);

File diff suppressed because it is too large Load diff

View file

@ -1,10 +0,0 @@
#pragma once
#include "lua.h"
int
context_get_server_info(lua_State*);
int
context_get_sink_info_list(lua_State*);
int
context_get_source_info_list(lua_State*);

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

@ -1,22 +1,18 @@
/// libpulse bindings. #include "pulseaudio.h"
//
// @module pulseaudio
#include "context.h"
#include "lua_util.h"
#include "proplist.h"
#include "volume.h"
#include <lauxlib.h>
#include <lua.h> #include <lua.h>
#include <lualib.h> #include <lualib.h>
#include <lauxlib.h>
#include <pulse/glib-mainloop.h> #include <pulse/glib-mainloop.h>
#include "pulseaudio.h"
#include "context.h"
#define LUA_MOD_EXPORT extern
#define LUA_PULSEAUDIO "pulseaudio"
#if LUA_VERSION_NUM <= 501 #if LUA_VERSION_NUM <= 501
// Shamelessly copied from Lua 5.3 source. // Shamelessly copied from Lua 5.3 source.
// TODO: What's the official way to do this in 5.1? void luaL_setfuncs(lua_State* L, const luaL_Reg* l, int nup) {
void luaL_setfuncs (lua_State *L, const luaL_Reg *l, int nup) {
luaL_checkstack(L, nup, "too many upvalues"); luaL_checkstack(L, nup, "too many upvalues");
for (; l->name != NULL; l++) { /* fill the table with given functions */ for (; l->name != NULL; l++) { /* fill the table with given functions */
if (l->func == NULL) /* place holder? */ if (l->func == NULL) /* place holder? */
@ -31,23 +27,9 @@ void luaL_setfuncs (lua_State *L, const luaL_Reg *l, int nup) {
} }
lua_pop(L, nup); /* remove upvalues */ lua_pop(L, nup); /* remove upvalues */
} }
#define luaL_newlib(L,l) (luaL_register(L,LUA_PULSEAUDIO,l))
#endif #endif
int pulseaudio_new(lua_State* L) {
/**
* Creates a new PulseAudio object.
*
* The API requires a GLib Main Context internally. Currently, only the default context
* is supported.
*
* @return[type=PulseAudio]
*/
int
pulseaudio_new(lua_State* L)
{
GMainContext* ctx = g_main_context_default(); GMainContext* ctx = g_main_context_default();
if (ctx == NULL) { if (ctx == NULL) {
lua_pushfstring(L, "Failed to accquire default GLib Main Context. Are we running in a Main Loop?"); lua_pushfstring(L, "Failed to accquire default GLib Main Context. Are we running in a Main Loop?");
@ -55,7 +37,7 @@ pulseaudio_new(lua_State* L)
return 0; return 0;
} }
pulseaudio* pa = lua_newuserdata (L, sizeof(pulseaudio)); pulseaudio* pa = lua_newuserdata(L, sizeof(pulseaudio));
if (!pa) { if (!pa) {
return luaL_error(L, "failed to create pulseaudio userdata"); return luaL_error(L, "failed to create pulseaudio userdata");
} }
@ -68,12 +50,10 @@ pulseaudio_new(lua_State* L)
} }
/** /*
* Proxies table index operations to our metatable. * Proxies table index operations to our metatable.
*/ */
int int pulseaudio__index(lua_State* L) {
pulseaudio__index(lua_State* L)
{
const char* index = luaL_checkstring(L, 2); const char* index = luaL_checkstring(L, 2);
luaL_getmetatable(L, LUA_PULSEAUDIO); luaL_getmetatable(L, LUA_PULSEAUDIO);
lua_getfield(L, -1, index); lua_getfield(L, -1, index);
@ -81,28 +61,83 @@ pulseaudio__index(lua_State* L)
} }
/** /*
* Free the PulseAudio object * Free the PulseAudio object
*/ */
int int pulseaudio__gc(lua_State* L) {
pulseaudio__gc(lua_State* L)
{
pulseaudio* pa = luaL_checkudata(L, 1, LUA_PULSEAUDIO); pulseaudio* pa = luaL_checkudata(L, 1, LUA_PULSEAUDIO);
pa_glib_mainloop_free(pa->mainloop); pa_glib_mainloop_free(pa->mainloop);
return 0; return 0;
} }
int int pulseaudio_new_context(lua_State* L) {
pulseaudio_new_context(lua_State* L)
{
pulseaudio* pa = luaL_checkudata(L, 1, LUA_PULSEAUDIO); pulseaudio* pa = luaL_checkudata(L, 1, LUA_PULSEAUDIO);
return context_new(L, pa_glib_mainloop_get_api(pa->mainloop)); return context_new(L, pa_glib_mainloop_get_api(pa->mainloop));
} }
LUA_MOD_EXPORT int luaopen_lua_libpulse_glib(lua_State* L) void createlib_volume(lua_State* L) {
{ luaL_newmetatable(L, LUA_PA_VOLUME);
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 // Create a table to store callback refs in, stored in the Lua registry
lua_pushstring(L, LUA_PULSEAUDIO); lua_pushstring(L, LUA_PULSEAUDIO);
lua_newtable(L); lua_newtable(L);
@ -111,12 +146,21 @@ LUA_MOD_EXPORT int luaopen_lua_libpulse_glib(lua_State* L)
lua_settable(L, -3); lua_settable(L, -3);
lua_rawset(L, LUA_REGISTRYINDEX); lua_rawset(L, LUA_REGISTRYINDEX);
luaL_newmetatable(L, LUA_PA_CONTEXT); createlib_context(L);
luaL_setfuncs(L, context_mt, 0); createlib_proplist(L);
createlib_pulseaudio(L);
luaL_newmetatable(L, LUA_PULSEAUDIO); return 1;
luaL_setfuncs(L, pulseaudio_mt, 0); }
luaL_newlib(L, pulseaudio_lib);
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; return 1;
} }

View file

@ -1,18 +1,16 @@
/** Bindings for PulseAudio's libpulse, using the GLib Main Loop.
*
* @module lua_libpulse_glib
*/
#pragma once #pragma once
#include "lua.h"
#include "lauxlib.h" #include "lauxlib.h"
#include "lua.h"
#include <pulse/glib-mainloop.h> #include <pulse/glib-mainloop.h>
#ifdef _WIN32 #define LUA_PULSEAUDIO "lua_libpulse_glib"
#define LUA_MOD_EXPORT __declspec(dllexport) #define LUA_PA_REGISTRY "lua_libpulse_glib.registry"
#else
#define LUA_MOD_EXPORT extern
#endif
#define LUA_PULSEAUDIO "pulseaudio"
#define LUA_PA_REGISTRY "pulseaudio.registry"
typedef struct pulseaudio { typedef struct pulseaudio {
@ -20,25 +18,43 @@ typedef struct pulseaudio {
} pulseaudio; } pulseaudio;
int /** Creates a new PulseAudio object.
pulseaudio_new(lua_State*); *
int * @function new
pulseaudio__gc(lua_State*); * @return[type=PulseAudio]
int */
pulseaudio__index(lua_State*); int pulseaudio_new(lua_State*);
int
pulseaudio_new_context(lua_State*);
static const struct luaL_Reg pulseaudio_mt [] = { int pulseaudio__gc(lua_State*);
{"__index", pulseaudio__index},
/// 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}, {"__gc", pulseaudio__gc},
{ NULL, NULL }
};
static const struct luaL_Reg pulseaudio_f[] = {
{"context", pulseaudio_new_context}, {"context", pulseaudio_new_context},
{NULL, NULL} { NULL, NULL }
}; };
static const struct luaL_Reg pulseaudio_lib [] = { static const struct luaL_Reg pulseaudio_lib[] = {
{"new", pulseaudio_new}, {"new", pulseaudio_new},
{NULL, NULL} { 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,46 +0,0 @@
local lgi = require("lgi")
local pulseaudio = require("lua_libpulse_glib")
local ppretty = require("pl.pretty")
local loop = lgi.GLib.MainLoop.new()
local pa = pulseaudio.new()
print("pulseaudio: ", pa)
local ctx = pa:context("test")
print("context: ", ctx)
local function context_state_to_string(state)
return ({
"unconnected",
"connecting",
"authorizing",
"setting_name",
"ready",
"failed",
"terminated"
})[state + 1]
end
print("calling connect")
ctx:connect("localhost", function(_, state)
print("pulse connection: ", context_state_to_string(state))
if state == 4 then
ctx:get_server_info(function(_, info)
print("server info")
ppretty.dump(info)
end)
ctx:get_sinks(function(_, list)
print("sinks")
ppretty.dump(list)
end)
ctx:get_sources(function(_, list)
print("sources")
ppretty.dump(list)
end)
end
end)
loop:run()

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")" mkdir -p "$(dirname "$OUT/$f")"
run "$LUA" ./tools/preprocessor.lua "$f" "$OUT/$f" run "$LUA" ./tools/preprocessor.lua "$f" "$OUT/$f"
done done