From d53e87bb67847c91c8176c3a8ae20862705c79e4 Mon Sep 17 00:00:00 2001 From: Ronald Y Date: Mon, 10 Jun 2024 20:22:43 +0800 Subject: [PATCH 1/2] pipewire group initial attempt --- packages/alsa-lib/build.sh | 18 + packages/alsa-utils/build.sh | 22 + packages/mpv/build.sh | 1 + packages/oboe/build.sh | 15 + ...IPTOS_DSCP.patch => 0001-IPTOS_DSCP.patch} | 0 ...cancel.patch => 0001-pthread_cancel.patch} | 0 ...llocarray.diff => 0001-reallocarray.patch} | 0 ...ntime_dir.patch => 0002-runtime_dir.patch} | 0 packages/pipewire/0003.patch | 200 ++++++ packages/pipewire/build.sh | 29 +- packages/pipewire/module-aaudio-sink.c | 565 +++++++++++++++++ packages/pipewire/module-aaudio-source.c | 488 +++++++++++++++ packages/pipewire/module-oboe-sink.cpp | 573 +++++++++++++++++ packages/pipewire/module-oboe-source.cpp | 586 ++++++++++++++++++ .../module-aaudio-sink.c | 203 ++++++ .../module-aaudio-source.c | 222 +++++++ .../module-protocol-pulse/module-oboe-sink.c | 208 +++++++ .../module-oboe-source.c | 210 +++++++ packages/qpwgraph/build.sh | 11 + 19 files changed, 3344 insertions(+), 7 deletions(-) create mode 100644 packages/alsa-lib/build.sh create mode 100644 packages/alsa-utils/build.sh create mode 100644 packages/oboe/build.sh rename packages/pipewire/{IPTOS_DSCP.patch => 0001-IPTOS_DSCP.patch} (100%) rename packages/pipewire/{pthread_cancel.patch => 0001-pthread_cancel.patch} (100%) rename packages/pipewire/{reallocarray.diff => 0001-reallocarray.patch} (100%) rename packages/pipewire/{runtime_dir.patch => 0002-runtime_dir.patch} (100%) create mode 100644 packages/pipewire/0003.patch create mode 100644 packages/pipewire/module-aaudio-sink.c create mode 100644 packages/pipewire/module-aaudio-source.c create mode 100644 packages/pipewire/module-oboe-sink.cpp create mode 100644 packages/pipewire/module-oboe-source.cpp create mode 100644 packages/pipewire/module-protocol-pulse/module-aaudio-sink.c create mode 100644 packages/pipewire/module-protocol-pulse/module-aaudio-source.c create mode 100644 packages/pipewire/module-protocol-pulse/module-oboe-sink.c create mode 100644 packages/pipewire/module-protocol-pulse/module-oboe-source.c create mode 100644 packages/qpwgraph/build.sh diff --git a/packages/alsa-lib/build.sh b/packages/alsa-lib/build.sh new file mode 100644 index 00000000000000..0e324bdcc6859f --- /dev/null +++ b/packages/alsa-lib/build.sh @@ -0,0 +1,18 @@ +TERMUX_PKG_HOMEPAGE=https://www.alsa-project.org +TERMUX_PKG_DESCRIPTION="The Advanced Linux Sound Architecture (ALSA) - library" +TERMUX_PKG_LICENSE="LGPL-2.1" +TERMUX_PKG_MAINTAINER="@termux" +TERMUX_PKG_VERSION="1.2.11" +TERMUX_PKG_SRCURL="https://github.com/alsa-project/alsa-lib/archive/refs/tags/v$TERMUX_PKG_VERSION.tar.gz" +TERMUX_PKG_SHA256=12216f0730d6dde3ded6a2a5388bc0009ad07f5c65972bd89aac9a76f8f085a4 +TERMUX_PKG_DEPENDS="libandroid-sysv-semaphore" +TERMUX_PKG_AUTO_UPDATE=true +TERMUX_PKG_EXTRA_CONFIGURE_ARGS=" +--disable-static +--with-versioned=no +" + +termux_step_pre_configure() { + LDFLAGS+=" -landroid-sysv-semaphore" + autoreconf -fi +} diff --git a/packages/alsa-utils/build.sh b/packages/alsa-utils/build.sh new file mode 100644 index 00000000000000..d5805ea5681534 --- /dev/null +++ b/packages/alsa-utils/build.sh @@ -0,0 +1,22 @@ +TERMUX_PKG_HOMEPAGE=https://www.alsa-project.org +TERMUX_PKG_DESCRIPTION="The Advanced Linux Sound Architecture (ALSA) - utils" +TERMUX_PKG_LICENSE="GPL-2.0" +TERMUX_PKG_MAINTAINER="@termux" +TERMUX_PKG_VERSION="1.2.11" +TERMUX_PKG_SRCURL="https://github.com/alsa-project/alsa-utils/archive/refs/tags/v$TERMUX_PKG_VERSION.tar.gz" +TERMUX_PKG_SHA256=978961153fa8ca4c783c93767e7054d0dc1fb42ef6f1008040ca71363d0f4d35 +TERMUX_PKG_AUTO_UPDATE=true +TERMUX_PKG_DEPENDS="ncurses" +TERMUX_PKG_BUILD_DEPENDS="alsa-lib" +TERMUX_PKG_EXTRA_CONFIGURE_ARGS=" +--with-udev-rules-dir=$TERMUX_PREFIX/lib/udev/rules.d +--with-asound-state-dir=$TERMUX_PREFIX/var/lib/alsa +--disable-bat +--disable-rst2man +" + +termux_step_pre_configure() { + LDFLAGS+=" -llog" + export ACLOCAL_PATH="${TERMUX_PREFIX}/share/aclocal" + autoreconf -fi +} diff --git a/packages/mpv/build.sh b/packages/mpv/build.sh index d645d183232a80..d231498ef5c5f8 100644 --- a/packages/mpv/build.sh +++ b/packages/mpv/build.sh @@ -8,6 +8,7 @@ TERMUX_PKG_SRCURL=https://github.com/mpv-player/mpv/archive/v${TERMUX_PKG_VERSIO TERMUX_PKG_SHA256=86d9ef40b6058732f67b46d0bbda24a074fae860b3eaae05bab3145041303066 TERMUX_PKG_AUTO_UPDATE=false TERMUX_PKG_DEPENDS="ffmpeg, libandroid-glob, libandroid-support, libarchive, libass, libcaca, libiconv, liblua52, libsixel, libuchardet, openal-soft, pulseaudio, rubberband, zlib, libplacebo" +TERMUX_PKG_MAKE_DEPENDS="jack, alsa-lib" TERMUX_PKG_RM_AFTER_INSTALL="share/icons share/applications" TERMUX_PKG_EXTRA_CONFIGURE_ARGS=" -Dlibmpv=true diff --git a/packages/oboe/build.sh b/packages/oboe/build.sh new file mode 100644 index 00000000000000..075412a62aabd8 --- /dev/null +++ b/packages/oboe/build.sh @@ -0,0 +1,15 @@ +TERMUX_PKG_HOMEPAGE=https://github.com/google/oboe +TERMUX_PKG_DESCRIPTION="Oboe is a C++ library that makes it easy to build high-performance audio apps on Android." +TERMUX_PKG_LICENSE="Apache-2.0" +TERMUX_PKG_MAINTAINER="@termux" +TERMUX_PKG_VERSION="1.8.1" +TERMUX_PKG_SRCURL="https://github.com/google/oboe/archive/refs/tags/$TERMUX_PKG_VERSION.tar.gz" +TERMUX_PKG_SHA256=af80c16175aa4602e51f3f4378424a199e5d91476b1cba6cd00299bf1e21881f +TERMUX_PKG_AUTO_UPDATE=true + +termux_step_post_make_install() { + TERMUX_PKG_EXTRA_CONFIGURE_ARGS="-DBUILD_SHARED_LIBS=TRUE" + termux_step_configure + termux_step_make + termux_step_make_install +} diff --git a/packages/pipewire/IPTOS_DSCP.patch b/packages/pipewire/0001-IPTOS_DSCP.patch similarity index 100% rename from packages/pipewire/IPTOS_DSCP.patch rename to packages/pipewire/0001-IPTOS_DSCP.patch diff --git a/packages/pipewire/pthread_cancel.patch b/packages/pipewire/0001-pthread_cancel.patch similarity index 100% rename from packages/pipewire/pthread_cancel.patch rename to packages/pipewire/0001-pthread_cancel.patch diff --git a/packages/pipewire/reallocarray.diff b/packages/pipewire/0001-reallocarray.patch similarity index 100% rename from packages/pipewire/reallocarray.diff rename to packages/pipewire/0001-reallocarray.patch diff --git a/packages/pipewire/runtime_dir.patch b/packages/pipewire/0002-runtime_dir.patch similarity index 100% rename from packages/pipewire/runtime_dir.patch rename to packages/pipewire/0002-runtime_dir.patch diff --git a/packages/pipewire/0003.patch b/packages/pipewire/0003.patch new file mode 100644 index 00000000000000..46cf0c421825c8 --- /dev/null +++ b/packages/pipewire/0003.patch @@ -0,0 +1,200 @@ +diff --git a/meson.build b/meson.build +index cda6011..4d423b7 100644 +--- a/meson.build ++++ b/meson.build +@@ -407,6 +407,15 @@ else + summary({'WebRTC Echo Canceling < 1.0': webrtc_dep.found()}, bool_yn: true, section: 'Misc dependencies') + endif + ++# On Android, AAudio and Oboe is required ++libaaudio_dep = (host_machine.system() == 'android' ++ ? cc.find_library('aaudio', required : true) ++ : dependency('', required: false)) ++ ++liboboe_dep = (host_machine.system() == 'android' ++ ? cc.find_library('oboe', required : true) ++ : dependency('', required: false)) ++ + # On FreeBSD and MidnightBSD, epoll-shim library is required for eventfd() and timerfd() + epoll_shim_dep = (host_machine.system() == 'freebsd' or host_machine.system() == 'midnightbsd' + ? dependency('epoll-shim', required: true) +diff --git a/pipewire-alsa/alsa-plugins/pcm_pipewire.c b/pipewire-alsa/alsa-plugins/pcm_pipewire.c +index 4ba25ee..7c3cacd 100644 +--- a/pipewire-alsa/alsa-plugins/pcm_pipewire.c ++++ b/pipewire-alsa/alsa-plugins/pcm_pipewire.c +@@ -1126,7 +1126,7 @@ static const struct pw_core_events core_events = { + }; + + +-static ssize_t log_write(void *cookie, const char *buf, size_t size) ++static int log_write(void *cookie, const char *buf, int size) + { + int len; + +@@ -1140,10 +1140,6 @@ static ssize_t log_write(void *cookie, const char *buf, size_t size) + return size; + } + +-static cookie_io_functions_t io_funcs = { +- .write = log_write, +-}; +- + static int execute_match(void *data, const char *location, const char *action, + const char *val, size_t len) + { +@@ -1169,7 +1165,7 @@ static int snd_pcm_pipewire_open(snd_pcm_t **pcmp, + pw->props = props; + pw->fd = -1; + pw->io.poll_fd = -1; +- pw->log_file = fopencookie(pw, "w", io_funcs); ++ pw->log_file = fwopen(pw, log_write); + if (pw->log_file == NULL) { + pw_log_error("can't create log file: %m"); + err = -errno; +diff --git a/pipewire-jack/src/pipewire-jack.c b/pipewire-jack/src/pipewire-jack.c +index 5a17d00..0722dd3 100644 +--- a/pipewire-jack/src/pipewire-jack.c ++++ b/pipewire-jack/src/pipewire-jack.c +@@ -7131,8 +7131,14 @@ int jack_client_kill_thread(jack_client_t* client, jack_native_thread_t thread) + + return_val_if_fail(client != NULL, -EINVAL); + ++#ifndef __ANDROID__ + pw_log_debug("cancel thread %p", (void *) thread); + pthread_cancel(thread); ++#else ++ // corresponding patches are done on the client side code (libjack) to accept SIGUSR2 ++ pw_log_debug("kill thread %p with SIGUSR2 (android)", (void *) thread); ++ pthread_kill(thread, SIGUSR2); ++#endif + pw_log_debug("join thread %p", (void *) thread); + spa_thread_utils_join(&c->context.thread_utils, (struct spa_thread*)thread, &status); + pw_log_debug("stopped thread %p", (void *) thread); +diff --git a/spa/plugins/alsa/alsa-pcm.c b/spa/plugins/alsa/alsa-pcm.c +index 55a152e..ef29c33 100644 +--- a/spa/plugins/alsa/alsa-pcm.c ++++ b/spa/plugins/alsa/alsa-pcm.c +@@ -626,7 +626,7 @@ int spa_alsa_parse_prop_params(struct state *state, struct spa_pod *params) + + #define CHECK(s,msg,...) if ((err = (s)) < 0) { spa_log_error(state->log, msg ": %s", ##__VA_ARGS__, snd_strerror(err)); return err; } + +-static ssize_t log_write(void *cookie, const char *buf, size_t size) ++static int log_write(void *cookie, const char *buf, int size) + { + struct state *state = cookie; + int len; +@@ -641,10 +641,6 @@ static ssize_t log_write(void *cookie, const char *buf, size_t size) + return size; + } + +-static cookie_io_functions_t io_funcs = { +- .write = log_write, +-}; +- + static void silence_error_handler(const char *file, int line, + const char *function, int err, const char *fmt, ...) + { +@@ -946,7 +942,7 @@ int spa_alsa_init(struct state *state, const struct spa_dict *info) + spa_log_error(state->log, "can't create card %u", state->card_index); + return -errno; + } +- state->log_file = fopencookie(state, "w", io_funcs); ++ state->log_file = fwopen(state, log_write); + if (state->log_file == NULL) { + spa_log_error(state->log, "can't create log file"); + return -errno; +diff --git a/src/daemon/pipewire-pulse.conf.in b/src/daemon/pipewire-pulse.conf.in +index 0d54cf9..9d7759e 100644 +--- a/src/daemon/pipewire-pulse.conf.in ++++ b/src/daemon/pipewire-pulse.conf.in +@@ -61,7 +61,8 @@ context.exec = [ + # args = " " + # ( flags = [ nofail ] ) + pulse.cmd = [ +- { cmd = "load-module" args = "module-always-sink" flags = [ ] } ++ { cmd = "load-module" args = "module-oboe-sink stream_write_timeout=0" flags = [ ] } ++ { cmd = "load-module" args = "module-oboe-source stream_read_timeout=0" flags = [ ] } + { cmd = "load-module" args = "module-device-manager" flags = [ ] } + { cmd = "load-module" args = "module-device-restore" flags = [ ] } + { cmd = "load-module" args = "module-stream-restore" flags = [ ] } +@@ -90,7 +91,7 @@ pulse.properties = { + server.address = [ + "unix:native" + #"unix:/tmp/something" # absolute paths may be used +- #"tcp:4713" # IPv4 and IPv6 on all addresses ++ "tcp:4713" # IPv4 and IPv6 on all addresses + #"tcp:[::]:9999" # IPv6 on all addresses + #"tcp:127.0.0.1:8888" # IPv4 on a single address + # +diff --git a/src/modules/meson.build b/src/modules/meson.build +index 922a384..cd74b6f 100644 +--- a/src/modules/meson.build ++++ b/src/modules/meson.build +@@ -4,6 +4,10 @@ subdir('spa') + # The list of "main" source files for modules, the ones that have the + # doxygen documentation + module_sources = [ ++ 'module-aaudio-source.c', ++ 'module-aaudio-sink.c', ++ 'module-oboe-sink.cpp', ++ 'module-aaudio-sink.cpp', + 'module-access.c', + 'module-adapter.c', + 'module-avb.c', +@@ -49,6 +53,45 @@ module_sources = [ + 'module-x11-bell.c', + ] + ++if libaaudio_dep.found() ++ pipewire_module_aaudio_sink = shared_library('pipewire-module-aaudio-sink', ++ [ 'module-aaudio-sink.c' ], ++ include_directories : [configinc], ++ install : true, ++ install_dir : modules_install_dir, ++ install_rpath: modules_install_dir, ++ dependencies : [spa_dep, mathlib, dl_lib, pipewire_dep, libaaudio_dep], ++ ) ++ ++ pipewire_module_aaudio_source = shared_library('pipewire-module-aaudio-source', ++ [ 'module-aaudio-source.c' ], ++ include_directories : [configinc], ++ install : true, ++ install_dir : modules_install_dir, ++ install_rpath: modules_install_dir, ++ dependencies : [spa_dep, mathlib, dl_lib, pipewire_dep, libaaudio_dep], ++ ) ++endif ++ ++if liboboe_dep.found() ++ pipewire_module_oboe_sink = shared_library('pipewire-module-oboe-sink', ++ [ 'module-oboe-sink.cpp' ], ++ include_directories : [configinc], ++ install : true, ++ install_dir : modules_install_dir, ++ install_rpath: modules_install_dir, ++ dependencies : [spa_dep, mathlib, dl_lib, pipewire_dep, liboboe_dep], ++ ) ++ pipewire_module_oboe_source = shared_library('pipewire-module-oboe-source', ++ [ 'module-oboe-source.cpp' ], ++ include_directories : [configinc], ++ install : true, ++ install_dir : modules_install_dir, ++ install_rpath: modules_install_dir, ++ dependencies : [spa_dep, mathlib, dl_lib, pipewire_dep, liboboe_dep], ++ ) ++endif ++ + pipewire_module_access_deps = [spa_dep, mathlib, dl_lib, pipewire_dep] + if flatpak_support + pipewire_module_access_deps += glib2_dep +@@ -376,6 +419,10 @@ pipewire_module_protocol_pulse_sources = [ + 'module-protocol-pulse/stream.c', + 'module-protocol-pulse/utils.c', + 'module-protocol-pulse/volume.c', ++ 'module-protocol-pulse/modules/module-aaudio-sink.c', ++ 'module-protocol-pulse/modules/module-aaudio-source.c', ++ 'module-protocol-pulse/modules/module-oboe-sink.c', ++ 'module-protocol-pulse/modules/module-oboe-source.c', + 'module-protocol-pulse/modules/module-alsa-sink.c', + 'module-protocol-pulse/modules/module-alsa-source.c', + 'module-protocol-pulse/modules/module-always-sink.c', diff --git a/packages/pipewire/build.sh b/packages/pipewire/build.sh index 57cd57388f3b03..8ec5f8d7cdd310 100644 --- a/packages/pipewire/build.sh +++ b/packages/pipewire/build.sh @@ -8,18 +8,18 @@ TERMUX_PKG_REVISION=1 TERMUX_PKG_SRCURL="https://gitlab.freedesktop.org/pipewire/pipewire/-/archive/${TERMUX_PKG_VERSION}/pipewire-${TERMUX_PKG_VERSION}.tar.bz2" TERMUX_PKG_SHA256=18ecba7174bf9f5da39cdf749e6cf260bd09b6831ba2f8165b20771cd10af4e5 TERMUX_PKG_AUTO_UPDATE=true -TERMUX_PKG_DEPENDS="ffmpeg, glib, libc++, liblua54, libopus, libsndfile, libwebrtc-audio-processing, lilv, ncurses, openssl, pulseaudio, readline" +TERMUX_PKG_DEPENDS="ffmpeg, glib, libc++, liblua54, libopus, libsndfile, libwebrtc-audio-processing, lilv, ncurses, openssl, readline, pulseaudio, alsa-lib, oboe" + # 'media-session' session-managers is disabled as it requires alsa. -# Since we are building without x11, dbus is disabled. TERMUX_PKG_EXTRA_CONFIGURE_ARGS=" -Dgstreamer=disabled -Dgstreamer-device-provider=disabled -Dtests=disabled -Dexamples=disabled --Dpipewire-alsa=disabled --Dalsa=disabled --Dpipewire-jack=disabled +-Dpipewire-alsa=enabled +-Dalsa=enabled +-Dpipewire-jack=enabled -Djack=disabled -Ddbus=disabled -Dsession-managers=['wireplumber'] @@ -28,7 +28,24 @@ TERMUX_PKG_EXTRA_CONFIGURE_ARGS=" -Dwireplumber:system-lua-version=54 " +termux_step_post_get_source() { + sed -i "s|@TERMUX_PKG_BUILDER_DIR@|${TERMUX_PKG_BUILDER_DIR}|g" \ + "${TERMUX_PKG_BUILDER_DIR}"/0001-reallocarray.patch +} + termux_step_pre_configure() { + # Our aaudio modules need libaaudio.so from a later android api version: + if [ $TERMUX_PKG_API_LEVEL -lt 26 ]; then + local _libdir="$TERMUX_PKG_TMPDIR/libaaudio" + rm -rf "${_libdir}" + mkdir -p "${_libdir}" + cp "$TERMUX_STANDALONE_TOOLCHAIN/sysroot/usr/lib/$TERMUX_HOST_PLATFORM/26/libaaudio.so" \ + "${_libdir}" + LDFLAGS+=" -L${_libdir}" + fi + cp $TERMUX_PKG_BUILDER_DIR/module-*.c* $TERMUX_PKG_SRCDIR/src/modules + cp $TERMUX_PKG_BUILDER_DIR/module-protocol-pulse/* $TERMUX_PKG_SRCDIR/src/modules/module-protocol-pulse/modules + local _WRAPPER_BIN="${TERMUX_PKG_BUILDDIR}/_wrapper/bin" mkdir -p "${_WRAPPER_BIN}" if [[ "${TERMUX_ON_DEVICE_BUILD}" == "false" ]]; then @@ -42,6 +59,4 @@ termux_step_pre_configure() { sed -i "s/'-Werror=strict-prototypes',//" ${TERMUX_PKG_SRCDIR}/meson.build CFLAGS+=" -Dindex=strchr -Drindex=strrchr" - sed "s|@TERMUX_PKG_BUILDER_DIR@|${TERMUX_PKG_BUILDER_DIR}|g" \ - "${TERMUX_PKG_BUILDER_DIR}"/reallocarray.diff | patch -p1 } diff --git a/packages/pipewire/module-aaudio-sink.c b/packages/pipewire/module-aaudio-sink.c new file mode 100644 index 00000000000000..cfcac3d50cadfe --- /dev/null +++ b/packages/pipewire/module-aaudio-sink.c @@ -0,0 +1,565 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#undef __INTRODUCED_IN +#define __INTRODUCED_IN(api_level) +#include +#define AAUDIO_NANOS_PER_MILLISECOND 1000000L + +/** \page page_module_aaudio_sink AAudio Sink + * + * ## Module Name + * + * `libpipewire-module-aaudio-sink` + * + * ## Module Options + * + * - `node.name`: a unique name for the stream + * - `node.description`: a human readable name for the stream + * - `stream.props = {}`: properties to be passed to the stream + * + * ## General options + * + * Options with well-known behavior. + * + * - \ref PW_KEY_REMOTE_NAME + * - \ref PW_KEY_AUDIO_FORMAT + * - \ref PW_KEY_AUDIO_RATE + * - \ref PW_KEY_AUDIO_CHANNELS + * - \ref SPA_KEY_AUDIO_POSITION + * - \ref PW_KEY_MEDIA_NAME + * - \ref PW_KEY_NODE_LATENCY + * - \ref PW_KEY_NODE_NAME + * - \ref PW_KEY_NODE_DESCRIPTION + * - \ref PW_KEY_NODE_GROUP + * - \ref PW_KEY_NODE_VIRTUAL + * - \ref PW_KEY_MEDIA_CLASS + * + * ## Configuration + * + *\code{.unparsed} + * context.modules = [ + * { name = libpipewire-module-aaudio-sink + * args = { + * node.name = "aaudio_sink" + * node.description = "My AAudio Sink" + * stream.props = { + * audio.position = [ FL FR ] + * } + * } + * } + * ] + *\endcode + */ + +#define NAME "aaudio-sink" + +PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME); +#define PW_LOG_TOPIC_DEFAULT mod_topic + +#define DEFAULT_FORMAT "S16LE" +#define DEFAULT_RATE 48000 +#define DEFAULT_CHANNELS 2 +#define DEFAULT_POSITION "[ FL FR ]" + +#define MODULE_USAGE "( node.latency= ) " \ + "( node.name= ) " \ + "( node.description= ) " \ + "( audio.format= ) " \ + "( audio.rate= ) " \ + "( audio.channels= ) " \ + "( audio.position= ) " \ + "( stream.props= ) " + + +static const struct spa_dict_item module_props[] = { + { PW_KEY_MODULE_AUTHOR, "Ronald Y" }, + { PW_KEY_MODULE_DESCRIPTION, "AAudio (Andoird) audio sink" }, + { PW_KEY_MODULE_USAGE, MODULE_USAGE }, + { PW_KEY_MODULE_VERSION, PACKAGE_VERSION }, +}; + +struct impl { + struct pw_context *context; + + struct pw_properties *props; + + struct pw_impl_module *module; + + struct spa_hook module_listener; + + struct pw_core *core; + struct spa_hook core_proxy_listener; + struct spa_hook core_listener; + + struct pw_properties *stream_props; + struct pw_stream *stream; + struct spa_hook stream_listener; + struct spa_audio_info_raw info; + uint32_t frame_size; + + unsigned int do_disconnect:1; + + AAudioStreamBuilder *aaudio_builder; + AAudioStream *aaudio_stream; +}; + +static void stream_destroy(void *d) +{ + struct impl *impl = d; + spa_hook_remove(&impl->stream_listener); + impl->stream = NULL; +} + +static void stream_state_changed(void *d, enum pw_stream_state old, + enum pw_stream_state state, const char *error) +{ + struct impl *impl = d; + switch (state) { + case PW_STREAM_STATE_ERROR: + case PW_STREAM_STATE_UNCONNECTED: + pw_log_debug("destroy triggered by stream state changed, pw_stream_state = %d", state); + AAudioStream_close(impl->aaudio_stream); + pw_impl_module_schedule_destroy(impl->module); + break; + case PW_STREAM_STATE_PAUSED: + case PW_STREAM_STATE_STREAMING: + break; + default: + break; + } +} + +static void playback_stream_process(void *d) +{ + struct impl *impl = d; + struct pw_buffer *buf; + struct spa_data *bd; + void *data; + uint32_t offs, size, i; + aaudio_result_t returnCode; + + if ((buf = pw_stream_dequeue_buffer(impl->stream)) == NULL) { + pw_log_debug("out of buffers: %m"); + return; + } + + for (i = 0; i < buf->buffer->n_datas; i++) { + bd = &buf->buffer->datas[i]; + + offs = SPA_MIN(bd->chunk->offset, bd->maxsize); + size = SPA_MIN(bd->maxsize - offs, bd->chunk->size); + + spa_zero(data); + data = SPA_PTROFF(bd->data, offs, void); + + // TODO: investigate timeout + if (returnCode = AAudioStream_write(impl->aaudio_stream, data, size / impl->frame_size, 200) < 0) + pw_log_error("AAudioStream_write error: %s", AAudio_convertResultToText(returnCode)); + } + pw_log_info("got buffer of size %d (= %d frames) and data %p", size, size / impl->frame_size, data); + + pw_stream_queue_buffer(impl->stream, buf); +} + +static const struct pw_stream_events playback_stream_events = { + PW_VERSION_STREAM_EVENTS, + .destroy = stream_destroy, + .state_changed = stream_state_changed, + .process = playback_stream_process +}; + + +static void core_destroy(void *d); + +static void error_callback(AAudioStream *stream, void *impl, aaudio_result_t error) { + pw_log_debug("AAudio error: %d", error); + core_destroy(impl); +} + +#define CHK(stmt) { \ + aaudio_result_t res = stmt; \ + if (res != AAUDIO_OK) { \ + pw_log_error("AAudio error %s at %s:%d\n", AAudio_convertResultToText(res), __FILE__, __LINE__); \ + goto fail; \ + } \ +} + +static int open_aaudio_stream(struct impl *impl) +{ + aaudio_format_t format; + + CHK(AAudio_createStreamBuilder(&impl->aaudio_builder)); + + AAudioStreamBuilder_setDirection(impl->aaudio_builder, AAUDIO_DIRECTION_OUTPUT); + + AAudioStreamBuilder_setPerformanceMode(impl->aaudio_builder, AAUDIO_PERFORMANCE_MODE_NONE); + AAudioStreamBuilder_setErrorCallback(impl->aaudio_builder, error_callback, impl); + + switch (impl->info.format) { + case SPA_AUDIO_FORMAT_S16_LE: format = AAUDIO_FORMAT_PCM_I16; break; + case SPA_AUDIO_FORMAT_S24_LE: format = AAUDIO_FORMAT_PCM_I24_PACKED; break; + case SPA_AUDIO_FORMAT_S32_LE: format = AAUDIO_FORMAT_PCM_I32; break; + case SPA_AUDIO_FORMAT_F32_LE: format = AAUDIO_FORMAT_PCM_FLOAT; break; + default: + pw_log_error( "audio format not supported. "); + goto fail; + } + AAudioStreamBuilder_setFormat(impl->aaudio_builder, format); + AAudioStreamBuilder_setSampleRate(impl->aaudio_builder, impl->info.rate); + AAudioStreamBuilder_setChannelCount(impl->aaudio_builder, impl->info.channels); + + CHK(AAudioStreamBuilder_openStream(impl->aaudio_builder, &impl->aaudio_stream)); + CHK(AAudioStreamBuilder_delete(impl->aaudio_builder)); + + impl->info.rate = AAudioStream_getSampleRate(impl->aaudio_stream); + CHK(AAudioStream_requestStart(impl->aaudio_stream)); + CHK(AAudioStream_waitForStateChange(impl->aaudio_stream, AAUDIO_STREAM_STATE_STARTING, NULL, 100 * AAUDIO_NANOS_PER_MILLISECOND)); + return 0; + +fail: + return -1; +} + + +static int create_stream(struct impl *impl) +{ + int res; + uint32_t n_params; + const struct spa_pod *params[1]; + uint8_t buffer[1024]; + struct spa_pod_builder b; + + open_aaudio_stream(impl); + impl->stream = pw_stream_new(impl->core, "aaudio sink", impl->stream_props); + impl->stream_props = NULL; + + if (impl->stream == NULL) + return -errno; + + pw_stream_add_listener(impl->stream, + &impl->stream_listener, + &playback_stream_events, impl); + + n_params = 0; + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + params[n_params++] = spa_format_audio_raw_build(&b, + SPA_PARAM_EnumFormat, &impl->info); + + if ((res = pw_stream_connect(impl->stream, + PW_DIRECTION_INPUT, + PW_ID_ANY, + PW_STREAM_FLAG_AUTOCONNECT | + PW_STREAM_FLAG_MAP_BUFFERS | + PW_STREAM_FLAG_RT_PROCESS, + params, n_params)) < 0) + return res; + + return 0; +} + +static void core_error(void *data, uint32_t id, int seq, int res, const char *message) +{ + struct impl *impl = data; + + pw_log_error("error id:%u seq:%d res:%d (%s): %s", + id, seq, res, spa_strerror(res), message); + + if (id == PW_ID_CORE && res == -EPIPE) { + AAudioStream_close(impl->aaudio_stream); + pw_impl_module_schedule_destroy(impl->module); + } +} + +static const struct pw_core_events core_events = { + PW_VERSION_CORE_EVENTS, + .error = core_error, +}; + +static void core_destroy(void *d) +{ + struct impl *impl = d; + AAudioStream_close(impl->aaudio_stream); + spa_hook_remove(&impl->core_listener); + impl->core = NULL; + pw_impl_module_schedule_destroy(impl->module); +} + +static const struct pw_proxy_events core_proxy_events = { + .destroy = core_destroy, +}; + +static void impl_destroy(struct impl *impl) +{ + if (impl->stream) + pw_stream_destroy(impl->stream); + if (impl->core && impl->do_disconnect) + pw_core_disconnect(impl->core); + + pw_properties_free(impl->stream_props); + pw_properties_free(impl->props); + + free(impl); +} + +static void module_destroy(void *data) +{ + struct impl *impl = data; + spa_hook_remove(&impl->module_listener); + impl_destroy(impl); +} + +static const struct pw_impl_module_events module_events = { + PW_VERSION_IMPL_MODULE_EVENTS, + .destroy = module_destroy, +}; + +static inline uint32_t format_from_name(const char *name, size_t len) +{ + int i; + for (i = 0; spa_type_audio_format[i].name; i++) { + if (strncmp(name, spa_debug_type_short_name(spa_type_audio_format[i].name), len) == 0) + return spa_type_audio_format[i].type; + } + return SPA_AUDIO_FORMAT_UNKNOWN; +} + +static uint32_t channel_from_name(const char *name) +{ + int i; + for (i = 0; spa_type_audio_channel[i].name; i++) { + if (spa_streq(name, spa_debug_type_short_name(spa_type_audio_channel[i].name))) + return spa_type_audio_channel[i].type; + } + return SPA_AUDIO_CHANNEL_UNKNOWN; +} + +static void parse_position(struct spa_audio_info_raw *info, const char *val, size_t len) +{ + struct spa_json it[2]; + char v[256]; + + spa_json_init(&it[0], val, len); + if (spa_json_enter_array(&it[0], &it[1]) <= 0) + spa_json_init(&it[1], val, len); + + info->channels = 0; + while (spa_json_get_string(&it[1], v, sizeof(v)) > 0 && + info->channels < SPA_AUDIO_MAX_CHANNELS) { + info->position[info->channels++] = channel_from_name(v); + } +} + +static void parse_audio_info(const struct pw_properties *props, struct spa_audio_info_raw *info) +{ + const char *str; + + spa_zero(*info); + if ((str = pw_properties_get(props, PW_KEY_AUDIO_FORMAT)) == NULL) + str = DEFAULT_FORMAT; + // TODO: enforce Android compatible format + info->format = format_from_name(str, strlen(str)); + switch (info->format) { + case SPA_AUDIO_FORMAT_S16_LE: + case SPA_AUDIO_FORMAT_S24_LE: + case SPA_AUDIO_FORMAT_S32_LE: + case SPA_AUDIO_FORMAT_F32_LE: + break; + default: + pw_log_error( "audio format not supported. fallback to SPA_AUDIO_FORMAT_S16_LE. "); + info->format = SPA_AUDIO_FORMAT_S16_LE; + } + + info->rate = pw_properties_get_uint32(props, PW_KEY_AUDIO_RATE, info->rate); + if (info->rate == 0) + info->rate = DEFAULT_RATE; + + info->channels = pw_properties_get_uint32(props, PW_KEY_AUDIO_CHANNELS, info->channels); + info->channels = SPA_MIN(info->channels, SPA_AUDIO_MAX_CHANNELS); + if ((str = pw_properties_get(props, SPA_KEY_AUDIO_POSITION)) != NULL) + parse_position(info, str, strlen(str)); + if (info->channels == 0) + parse_position(info, DEFAULT_POSITION, strlen(DEFAULT_POSITION)); +} + +static int calc_frame_size(const struct spa_audio_info_raw *info) +{ + int res = info->channels; + switch (info->format) { + case SPA_AUDIO_FORMAT_U8: + case SPA_AUDIO_FORMAT_S8: + case SPA_AUDIO_FORMAT_ALAW: + case SPA_AUDIO_FORMAT_ULAW: + return res; + case SPA_AUDIO_FORMAT_S16: + case SPA_AUDIO_FORMAT_S16_OE: + case SPA_AUDIO_FORMAT_U16: + return res * 2; + case SPA_AUDIO_FORMAT_S24: + case SPA_AUDIO_FORMAT_S24_OE: + case SPA_AUDIO_FORMAT_U24: + return res * 3; + case SPA_AUDIO_FORMAT_S24_32: + case SPA_AUDIO_FORMAT_S24_32_OE: + case SPA_AUDIO_FORMAT_S32: + case SPA_AUDIO_FORMAT_S32_OE: + case SPA_AUDIO_FORMAT_U32: + case SPA_AUDIO_FORMAT_U32_OE: + case SPA_AUDIO_FORMAT_F32: + case SPA_AUDIO_FORMAT_F32_OE: + return res * 4; + case SPA_AUDIO_FORMAT_F64: + case SPA_AUDIO_FORMAT_F64_OE: + return res * 8; + default: + return 0; + } +} + +static void copy_props(struct impl *impl, struct pw_properties *props, const char *key) +{ + const char *str; + if ((str = pw_properties_get(props, key)) != NULL) { + if (pw_properties_get(impl->stream_props, key) == NULL) + pw_properties_set(impl->stream_props, key, str); + } +} + +SPA_EXPORT +int pipewire__module_init(struct pw_impl_module *module, const char *args) +{ + struct pw_context *context = pw_impl_module_get_context(module); + struct pw_properties *props = NULL; + uint32_t id = pw_global_get_id(pw_impl_module_get_global(module)); + uint32_t pid = getpid(); + struct impl *impl; + const char *str; + int res; + + PW_LOG_TOPIC_INIT(mod_topic); + + impl = calloc(1, sizeof(struct impl)); + if (impl == NULL) + return -errno; + + pw_log_debug("module %p: new %s", impl, args); + + if (args == NULL) + args = ""; + + props = pw_properties_new_string(args); + if (props == NULL) { + res = -errno; + pw_log_error( "can't create properties: %m"); + goto error; + } + impl->props = props; + + impl->stream_props = pw_properties_new(NULL, NULL); + if (impl->stream_props == NULL) { + res = -errno; + pw_log_error( "can't create properties: %m"); + goto error; + } + + impl->module = module; + impl->context = context; + + if (pw_properties_get(props, PW_KEY_NODE_VIRTUAL) == NULL) + pw_properties_set(props, PW_KEY_NODE_VIRTUAL, "true"); + + if (pw_properties_get(props, PW_KEY_MEDIA_CLASS) == NULL) + pw_properties_set(props, PW_KEY_MEDIA_CLASS, "Audio/Sink"); + + if (pw_properties_get(props, PW_KEY_NODE_NAME) == NULL) + pw_properties_setf(props, PW_KEY_NODE_NAME, "aaudio-sink-%u-%u", pid, id); + if (pw_properties_get(props, PW_KEY_NODE_DESCRIPTION) == NULL) + pw_properties_set(props, PW_KEY_NODE_DESCRIPTION, + pw_properties_get(props, PW_KEY_NODE_NAME)); + + if ((str = pw_properties_get(props, "stream.props")) != NULL) + pw_properties_update_string(impl->stream_props, str, strlen(str)); + + copy_props(impl, props, PW_KEY_AUDIO_RATE); + copy_props(impl, props, PW_KEY_AUDIO_CHANNELS); + copy_props(impl, props, SPA_KEY_AUDIO_POSITION); + copy_props(impl, props, PW_KEY_NODE_NAME); + copy_props(impl, props, PW_KEY_NODE_DESCRIPTION); + copy_props(impl, props, PW_KEY_NODE_GROUP); + copy_props(impl, props, PW_KEY_NODE_LATENCY); + copy_props(impl, props, PW_KEY_NODE_VIRTUAL); + copy_props(impl, props, PW_KEY_MEDIA_CLASS); + + parse_audio_info(impl->stream_props, &impl->info); + + impl->frame_size = calc_frame_size(&impl->info); + if (impl->frame_size == 0) { + res = -EINVAL; + pw_log_error( "can't parse audio format"); + goto error; + } + + impl->core = pw_context_get_object(impl->context, PW_TYPE_INTERFACE_Core); + if (impl->core == NULL) { + str = pw_properties_get(props, PW_KEY_REMOTE_NAME); + impl->core = pw_context_connect(impl->context, + pw_properties_new( + PW_KEY_REMOTE_NAME, str, + NULL), + 0); + impl->do_disconnect = true; + } + if (impl->core == NULL) { + res = -errno; + pw_log_error("can't connect: %m"); + goto error; + } + + pw_proxy_add_listener((struct pw_proxy*)impl->core, + &impl->core_proxy_listener, + &core_proxy_events, impl); + pw_core_add_listener(impl->core, + &impl->core_listener, + &core_events, impl); + + if ((res = create_stream(impl)) < 0) + goto error; + + pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl); + + pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props)); + + return 0; + +error: + impl_destroy(impl); + return res; +} diff --git a/packages/pipewire/module-aaudio-source.c b/packages/pipewire/module-aaudio-source.c new file mode 100644 index 00000000000000..ec007855e6c165 --- /dev/null +++ b/packages/pipewire/module-aaudio-source.c @@ -0,0 +1,488 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +/** \page page_module_example_source Example Source + * + * The example source is a good starting point for writing a custom + * source. We refer to the source code for more information. + * + * ## Module Name + * + * `libpipewire-module-example-source` + * + * ## Module Options + * + * - `node.name`: a unique name for the stream + * - `node.description`: a human readable name for the stream + * - `stream.props = {}`: properties to be passed to the stream + * + * ## General options + * + * Options with well-known behavior. + * + * - \ref PW_KEY_REMOTE_NAME + * - \ref PW_KEY_AUDIO_FORMAT + * - \ref PW_KEY_AUDIO_RATE + * - \ref PW_KEY_AUDIO_CHANNELS + * - \ref SPA_KEY_AUDIO_POSITION + * - \ref PW_KEY_MEDIA_NAME + * - \ref PW_KEY_NODE_LATENCY + * - \ref PW_KEY_NODE_NAME + * - \ref PW_KEY_NODE_DESCRIPTION + * - \ref PW_KEY_NODE_GROUP + * - \ref PW_KEY_NODE_VIRTUAL + * - \ref PW_KEY_MEDIA_CLASS + * + * ## Example configuration + * + *\code{.unparsed} + * context.modules = [ + * { name = libpipewire-module-example-source + * args = { + * node.name = "example_source" + * node.description = "My Example Source" + * stream.props = { + * audio.position = [ FL FR ] + * } + * } + * } + * ] + *\endcode + */ + +#define NAME "example-source" + +PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME); +#define PW_LOG_TOPIC_DEFAULT mod_topic + +#define DEFAULT_FORMAT "S16" +#define DEFAULT_RATE 48000 +#define DEFAULT_CHANNELS 2 +#define DEFAULT_POSITION "[ FL FR ]" + +#define MODULE_USAGE "( node.latency= ) " \ + "( node.name= ) " \ + "( node.description= ) " \ + "( audio.format= ) " \ + "( audio.rate= ) " \ + "( audio.channels= ) " \ + "( audio.position= ) " \ + "( stream.props= ) " + + +static const struct spa_dict_item module_props[] = { + { PW_KEY_MODULE_AUTHOR, "Wim Taymans " }, + { PW_KEY_MODULE_DESCRIPTION, "An example audio source" }, + { PW_KEY_MODULE_USAGE, MODULE_USAGE }, + { PW_KEY_MODULE_VERSION, PACKAGE_VERSION }, +}; + +struct impl { + struct pw_context *context; + + struct pw_properties *props; + + struct pw_impl_module *module; + + struct spa_hook module_listener; + + struct pw_core *core; + struct spa_hook core_proxy_listener; + struct spa_hook core_listener; + + struct pw_properties *stream_props; + struct pw_stream *stream; + struct spa_hook stream_listener; + struct spa_audio_info_raw info; + uint32_t frame_size; + + unsigned int do_disconnect:1; + unsigned int unloading:1; +}; + +static void stream_destroy(void *d) +{ + struct impl *impl = d; + spa_hook_remove(&impl->stream_listener); + impl->stream = NULL; +} + +static void stream_state_changed(void *d, enum pw_stream_state old, + enum pw_stream_state state, const char *error) +{ + struct impl *impl = d; + switch (state) { + case PW_STREAM_STATE_ERROR: + case PW_STREAM_STATE_UNCONNECTED: + pw_impl_module_schedule_destroy(impl->module); + break; + case PW_STREAM_STATE_PAUSED: + case PW_STREAM_STATE_STREAMING: + break; + default: + break; + } +} + +static void capture_stream_process(void *d) +{ + struct impl *impl = d; + struct pw_buffer *buf; + struct spa_data *bd; + void *data; + uint32_t size; + + if ((buf = pw_stream_dequeue_buffer(impl->stream)) == NULL) { + pw_log_debug("out of buffers: %m"); + return; + } + + bd = &buf->buffer->datas[0]; + + data = bd->data; + size = buf->requested ? buf->requested * impl->frame_size : bd->maxsize; + + /* fill buffer contents here */ + pw_log_info("fill buffer data %p with up to %u bytes", data, size); + + bd->chunk->size = size; + bd->chunk->stride = impl->frame_size; + bd->chunk->offset = 0; + buf->size = size / impl->frame_size; + + pw_stream_queue_buffer(impl->stream, buf); +} + +static const struct pw_stream_events capture_stream_events = { + PW_VERSION_STREAM_EVENTS, + .destroy = stream_destroy, + .state_changed = stream_state_changed, + .process = capture_stream_process +}; + +static int create_stream(struct impl *impl) +{ + int res; + uint32_t n_params; + const struct spa_pod *params[1]; + uint8_t buffer[1024]; + struct spa_pod_builder b; + + impl->stream = pw_stream_new(impl->core, "example source", impl->stream_props); + impl->stream_props = NULL; + + if (impl->stream == NULL) + return -errno; + + pw_stream_add_listener(impl->stream, + &impl->stream_listener, + &capture_stream_events, impl); + + n_params = 0; + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + params[n_params++] = spa_format_audio_raw_build(&b, + SPA_PARAM_EnumFormat, &impl->info); + + if ((res = pw_stream_connect(impl->stream, + PW_DIRECTION_OUTPUT, + PW_ID_ANY, + PW_STREAM_FLAG_AUTOCONNECT | + PW_STREAM_FLAG_MAP_BUFFERS | + PW_STREAM_FLAG_RT_PROCESS, + params, n_params)) < 0) + return res; + + return 0; +} + +static void core_error(void *data, uint32_t id, int seq, int res, const char *message) +{ + struct impl *impl = data; + + pw_log_error("error id:%u seq:%d res:%d (%s): %s", + id, seq, res, spa_strerror(res), message); + + if (id == PW_ID_CORE && res == -EPIPE) + pw_impl_module_schedule_destroy(impl->module); +} + +static const struct pw_core_events core_events = { + PW_VERSION_CORE_EVENTS, + .error = core_error, +}; + +static void core_destroy(void *d) +{ + struct impl *impl = d; + spa_hook_remove(&impl->core_listener); + impl->core = NULL; + pw_impl_module_schedule_destroy(impl->module); +} + +static const struct pw_proxy_events core_proxy_events = { + .destroy = core_destroy, +}; + +static void impl_destroy(struct impl *impl) +{ + if (impl->stream) + pw_stream_destroy(impl->stream); + if (impl->core && impl->do_disconnect) + pw_core_disconnect(impl->core); + + pw_properties_free(impl->stream_props); + pw_properties_free(impl->props); + + free(impl); +} + +static void module_destroy(void *data) +{ + struct impl *impl = data; + impl->unloading = true; + spa_hook_remove(&impl->module_listener); + impl_destroy(impl); +} + +static const struct pw_impl_module_events module_events = { + PW_VERSION_IMPL_MODULE_EVENTS, + .destroy = module_destroy, +}; + +static inline uint32_t format_from_name(const char *name, size_t len) +{ + int i; + for (i = 0; spa_type_audio_format[i].name; i++) { + if (strncmp(name, spa_debug_type_short_name(spa_type_audio_format[i].name), len) == 0) + return spa_type_audio_format[i].type; + } + return SPA_AUDIO_FORMAT_UNKNOWN; +} + +static uint32_t channel_from_name(const char *name) +{ + int i; + for (i = 0; spa_type_audio_channel[i].name; i++) { + if (spa_streq(name, spa_debug_type_short_name(spa_type_audio_channel[i].name))) + return spa_type_audio_channel[i].type; + } + return SPA_AUDIO_CHANNEL_UNKNOWN; +} + +static void parse_position(struct spa_audio_info_raw *info, const char *val, size_t len) +{ + struct spa_json it[2]; + char v[256]; + + spa_json_init(&it[0], val, len); + if (spa_json_enter_array(&it[0], &it[1]) <= 0) + spa_json_init(&it[1], val, len); + + info->channels = 0; + while (spa_json_get_string(&it[1], v, sizeof(v)) > 0 && + info->channels < SPA_AUDIO_MAX_CHANNELS) { + info->position[info->channels++] = channel_from_name(v); + } +} + +static void parse_audio_info(const struct pw_properties *props, struct spa_audio_info_raw *info) +{ + const char *str; + + spa_zero(*info); + if ((str = pw_properties_get(props, PW_KEY_AUDIO_FORMAT)) == NULL) + str = DEFAULT_FORMAT; + info->format = format_from_name(str, strlen(str)); + + info->rate = pw_properties_get_uint32(props, PW_KEY_AUDIO_RATE, info->rate); + if (info->rate == 0) + info->rate = DEFAULT_RATE; + + info->channels = pw_properties_get_uint32(props, PW_KEY_AUDIO_CHANNELS, info->channels); + info->channels = SPA_MIN(info->channels, SPA_AUDIO_MAX_CHANNELS); + if ((str = pw_properties_get(props, SPA_KEY_AUDIO_POSITION)) != NULL) + parse_position(info, str, strlen(str)); + if (info->channels == 0) + parse_position(info, DEFAULT_POSITION, strlen(DEFAULT_POSITION)); +} + +static int calc_frame_size(const struct spa_audio_info_raw *info) +{ + int res = info->channels; + switch (info->format) { + case SPA_AUDIO_FORMAT_U8: + case SPA_AUDIO_FORMAT_S8: + case SPA_AUDIO_FORMAT_ALAW: + case SPA_AUDIO_FORMAT_ULAW: + return res; + case SPA_AUDIO_FORMAT_S16: + case SPA_AUDIO_FORMAT_S16_OE: + case SPA_AUDIO_FORMAT_U16: + return res * 2; + case SPA_AUDIO_FORMAT_S24: + case SPA_AUDIO_FORMAT_S24_OE: + case SPA_AUDIO_FORMAT_U24: + return res * 3; + case SPA_AUDIO_FORMAT_S24_32: + case SPA_AUDIO_FORMAT_S24_32_OE: + case SPA_AUDIO_FORMAT_S32: + case SPA_AUDIO_FORMAT_S32_OE: + case SPA_AUDIO_FORMAT_U32: + case SPA_AUDIO_FORMAT_U32_OE: + case SPA_AUDIO_FORMAT_F32: + case SPA_AUDIO_FORMAT_F32_OE: + return res * 4; + case SPA_AUDIO_FORMAT_F64: + case SPA_AUDIO_FORMAT_F64_OE: + return res * 8; + default: + return 0; + } +} + +static void copy_props(struct impl *impl, struct pw_properties *props, const char *key) +{ + const char *str; + if ((str = pw_properties_get(props, key)) != NULL) { + if (pw_properties_get(impl->stream_props, key) == NULL) + pw_properties_set(impl->stream_props, key, str); + } +} + +SPA_EXPORT +int pipewire__module_init(struct pw_impl_module *module, const char *args) +{ + struct pw_context *context = pw_impl_module_get_context(module); + uint32_t id = pw_global_get_id(pw_impl_module_get_global(module)); + uint32_t pid = getpid(); + struct pw_properties *props = NULL; + struct impl *impl; + const char *str; + int res; + + PW_LOG_TOPIC_INIT(mod_topic); + + impl = calloc(1, sizeof(struct impl)); + if (impl == NULL) + return -errno; + + pw_log_debug("module %p: new %s", impl, args); + + if (args == NULL) + args = ""; + + props = pw_properties_new_string(args); + if (props == NULL) { + res = -errno; + pw_log_error( "can't create properties: %m"); + goto error; + } + impl->props = props; + + impl->stream_props = pw_properties_new(NULL, NULL); + if (impl->stream_props == NULL) { + res = -errno; + pw_log_error( "can't create properties: %m"); + goto error; + } + + impl->module = module; + impl->context = context; + + if (pw_properties_get(props, PW_KEY_NODE_VIRTUAL) == NULL) + pw_properties_set(props, PW_KEY_NODE_VIRTUAL, "true"); + + if (pw_properties_get(props, PW_KEY_MEDIA_CLASS) == NULL) + pw_properties_set(props, PW_KEY_MEDIA_CLASS, "Audio/Source"); + + if (pw_properties_get(props, PW_KEY_NODE_NAME) == NULL) + pw_properties_setf(props, PW_KEY_NODE_NAME, "example-source-%u-%u", pid, id); + if (pw_properties_get(props, PW_KEY_NODE_DESCRIPTION) == NULL) + pw_properties_set(props, PW_KEY_NODE_DESCRIPTION, + pw_properties_get(props, PW_KEY_NODE_NAME)); + + if ((str = pw_properties_get(props, "stream.props")) != NULL) + pw_properties_update_string(impl->stream_props, str, strlen(str)); + + copy_props(impl, props, PW_KEY_AUDIO_RATE); + copy_props(impl, props, PW_KEY_AUDIO_CHANNELS); + copy_props(impl, props, SPA_KEY_AUDIO_POSITION); + copy_props(impl, props, PW_KEY_NODE_NAME); + copy_props(impl, props, PW_KEY_NODE_DESCRIPTION); + copy_props(impl, props, PW_KEY_NODE_GROUP); + copy_props(impl, props, PW_KEY_NODE_LATENCY); + copy_props(impl, props, PW_KEY_NODE_VIRTUAL); + copy_props(impl, props, PW_KEY_MEDIA_CLASS); + + parse_audio_info(impl->stream_props, &impl->info); + + impl->frame_size = calc_frame_size(&impl->info); + if (impl->frame_size == 0) { + res = -EINVAL; + pw_log_error( "can't parse audio format"); + goto error; + } + + impl->core = pw_context_get_object(impl->context, PW_TYPE_INTERFACE_Core); + if (impl->core == NULL) { + str = pw_properties_get(props, PW_KEY_REMOTE_NAME); + impl->core = pw_context_connect(impl->context, + pw_properties_new( + PW_KEY_REMOTE_NAME, str, + NULL), + 0); + impl->do_disconnect = true; + } + if (impl->core == NULL) { + res = -errno; + pw_log_error("can't connect: %m"); + goto error; + } + + pw_proxy_add_listener((struct pw_proxy*)impl->core, + &impl->core_proxy_listener, + &core_proxy_events, impl); + pw_core_add_listener(impl->core, + &impl->core_listener, + &core_events, impl); + + if ((res = create_stream(impl)) < 0) + goto error; + + pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl); + + pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props)); + + return 0; + +error: + impl_destroy(impl); + return res; +} diff --git a/packages/pipewire/module-oboe-sink.cpp b/packages/pipewire/module-oboe-sink.cpp new file mode 100644 index 00000000000000..91baa52f6b776c --- /dev/null +++ b/packages/pipewire/module-oboe-sink.cpp @@ -0,0 +1,573 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include + +extern "C" { + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#define NANOS_PER_MILLISECOND 1000000L + +/** \page page_module_oboe_sink Oboe Sink + * + * ## Module Name + * + * `libpipewire-module-oboe-sink` + * + * ## Module Options + * + * - `node.name`: a unique name for the stream + * - `node.description`: a human readable name for the stream + * - `stream.props = {}`: properties to be passed to the stream + * + * ## General options + * + * Options with well-known behavior. + * + * - \ref PW_KEY_REMOTE_NAME + * - \ref PW_KEY_AUDIO_FORMAT + * - \ref PW_KEY_AUDIO_RATE + * - \ref PW_KEY_AUDIO_CHANNELS + * - \ref SPA_KEY_AUDIO_POSITION + * - \ref PW_KEY_MEDIA_NAME + * - \ref PW_KEY_NODE_LATENCY + * - \ref PW_KEY_NODE_NAME + * - \ref PW_KEY_NODE_DESCRIPTION + * - \ref PW_KEY_NODE_GROUP + * - \ref PW_KEY_NODE_VIRTUAL + * - \ref PW_KEY_MEDIA_CLASS + * + * ## Configuration + * + *\code{.unparsed} + * context.modules = [ + * { name = libpipewire-module-oboe-sink + * args = { + * node.name = "oboe_sink" + * node.description = "My Oboe Sink" + * stream.props = { + * audio.position = [ FL FR ] + * } + * } + * } + * ] + *\endcode + */ + +#define NAME "oboe-sink" + +PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME); +#define PW_LOG_TOPIC_DEFAULT mod_topic + +#define DEFAULT_FORMAT "S16LE" +#define DEFAULT_RATE 48000 +#define DEFAULT_CHANNELS 2 +#define DEFAULT_POSITION "[ FL FR ]" +#define DEFAULT_STREAM_WRITE_TIMEOUT 0 + +#define MODULE_USAGE "( node.latency= ) " \ + "( node.name= ) " \ + "( node.description= ) " \ + "( audio.format= ) " \ + "( audio.rate= ) " \ + "( audio.channels= ) " \ + "( audio.position= ) " \ + "( stream.write.timeout= ) " \ + "( stream.props= ) " + + +static const struct spa_dict_item module_props[] = { + { PW_KEY_MODULE_AUTHOR, "Ronald Y" }, + { PW_KEY_MODULE_DESCRIPTION, "Oboe (Andoird) audio sink" }, + { PW_KEY_MODULE_USAGE, MODULE_USAGE }, + { PW_KEY_MODULE_VERSION, PACKAGE_VERSION }, +}; + +struct impl { + struct pw_context *context; + + struct pw_properties *props; + + struct pw_impl_module *module; + + struct spa_hook module_listener; + + struct pw_core *core; + struct spa_hook core_proxy_listener; + struct spa_hook core_listener; + + struct pw_properties *stream_props; + struct pw_stream *stream; + struct spa_hook stream_listener; + struct spa_audio_info_raw info; + uint32_t frame_size; + int64_t stream_write_timeout; + + unsigned int do_disconnect:1; + + std::shared_ptr oboe_stream; +}; + +static void stream_destroy(void *d) +{ + struct impl *impl = (struct impl *)d; + spa_hook_remove(&impl->stream_listener); + impl->stream = NULL; +} + +static void stream_state_changed(void *d, enum pw_stream_state old, + enum pw_stream_state state, const char *error) +{ + struct impl *impl = (struct impl *)d; + switch (state) { + case PW_STREAM_STATE_ERROR: + case PW_STREAM_STATE_UNCONNECTED: + pw_log_debug("destroy triggered by stream state changed, pw_stream_state = %d", state); + impl->oboe_stream->close(); + pw_impl_module_schedule_destroy(impl->module); + break; + case PW_STREAM_STATE_PAUSED: + case PW_STREAM_STATE_STREAMING: + break; + default: + break; + } +} + +static int open_oboe_stream(struct impl *impl); + +static void playback_stream_process(void *d) +{ + struct impl *impl = (struct impl *)d; + struct pw_buffer *buf; + struct spa_data *bd; + void *data; + uint32_t offs, size, i; + oboe::Result returnCode; + + if ((buf = pw_stream_dequeue_buffer(impl->stream)) == NULL) { + pw_log_debug("out of buffers: %m"); + return; + } + + for (i = 0; i < buf->buffer->n_datas; i++) { + bd = &buf->buffer->datas[i]; + + offs = SPA_MIN(bd->chunk->offset, bd->maxsize); + size = SPA_MIN(bd->maxsize - offs, bd->chunk->size); + + spa_zero(data); + data = SPA_PTROFF(bd->data, offs, void); + + // TODO: investigate timeout + if ((returnCode = impl->oboe_stream->write(data, size / impl->frame_size, impl->stream_write_timeout)) != oboe::Result::OK) + pw_log_error("Oboe stream write() error: %s", oboe::convertToText(returnCode)); + if (returnCode == oboe::Result::ErrorDisconnected) + open_oboe_stream(impl); + } + pw_log_info("got buffer of size %d (= %d frames) and data %p", size, size / impl->frame_size, data); + + pw_stream_queue_buffer(impl->stream, buf); +} + +static const struct pw_stream_events playback_stream_events = { + PW_VERSION_STREAM_EVENTS, + .destroy = stream_destroy, + .state_changed = stream_state_changed, + .process = playback_stream_process +}; + + +// static void core_destroy(void *d); + +// static void error_callback(std::shared_ptr stream, void *impl, oboe::Result error) { +// pw_log_debug("Oboe error: %d", error); +// core_destroy(impl); +// } + +#define CHK(stmt) { \ + oboe::Result res = stmt; \ + if (res != oboe::Result::OK) { \ + pw_log_error("Oboe error %s at %s:%d\n", oboe::convertToText(res), __FILE__, __LINE__); \ + goto fail; \ + } \ +} + +static int open_oboe_stream(struct impl *impl) +{ + oboe::AudioFormat format; + oboe::AudioStreamBuilder builder; + switch (impl->info.format) { + case SPA_AUDIO_FORMAT_S16_LE: format = oboe::AudioFormat::I16; break; + case SPA_AUDIO_FORMAT_F32_LE: format = oboe::AudioFormat::Float; break; + default: + pw_log_error( "audio format not supported. "); + goto fail; + } + + CHK(builder.setDirection(oboe::Direction::Output) + ->setSharingMode(oboe::SharingMode::Shared) + ->setChannelCount(impl->info.channels) + ->setSampleRate(impl->info.rate) + ->setSampleRateConversionQuality(oboe::SampleRateConversionQuality::Medium) + ->setFormat(format) + // TODO: error callback + // ->setErrorCallback(error_callback) + ->openStream(impl->oboe_stream)); + + impl->info.rate = impl->oboe_stream->getSampleRate(); + + CHK(impl->oboe_stream->requestStart()); + CHK(impl->oboe_stream->waitForStateChange(oboe::StreamState::Starting, NULL, 1000 * NANOS_PER_MILLISECOND)); + return 0; + +fail: + return -1; +} + + +static int create_stream(struct impl *impl) +{ + int res; + uint32_t n_params; + const struct spa_pod *params[1]; + uint8_t buffer[1024]; + struct spa_pod_builder b; + + open_oboe_stream(impl); + impl->stream = pw_stream_new(impl->core, "oboe sink", impl->stream_props); + impl->stream_props = NULL; + + if (impl->stream == NULL) + return -errno; + + pw_stream_add_listener(impl->stream, + &impl->stream_listener, + &playback_stream_events, impl); + + n_params = 0; + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + params[n_params++] = spa_format_audio_raw_build(&b, + SPA_PARAM_EnumFormat, &impl->info); + + if ((res = pw_stream_connect(impl->stream, + PW_DIRECTION_INPUT, + PW_ID_ANY, + static_cast(PW_STREAM_FLAG_AUTOCONNECT | + PW_STREAM_FLAG_MAP_BUFFERS | + PW_STREAM_FLAG_RT_PROCESS), + params, n_params)) < 0) + return res; + + return 0; +} + +static void core_error(void *data, uint32_t id, int seq, int res, const char *message) +{ + struct impl *impl = (struct impl *)data; + + pw_log_error("error id:%u seq:%d res:%d (%s): %s", + id, seq, res, spa_strerror(res), message); + + if (id == PW_ID_CORE && res == -EPIPE) { + impl->oboe_stream->close(); + pw_impl_module_schedule_destroy(impl->module); + } +} + +static const struct pw_core_events core_events = { + PW_VERSION_CORE_EVENTS, + .error = core_error, +}; + +static void core_destroy(void *d) +{ + struct impl *impl = (struct impl *)d; + impl->oboe_stream->close(); + spa_hook_remove(&impl->core_listener); + impl->core = NULL; + pw_impl_module_schedule_destroy(impl->module); +} + +static const struct pw_proxy_events core_proxy_events = { + .destroy = core_destroy, +}; + +static void impl_destroy(struct impl *impl) +{ + if (impl->stream) + pw_stream_destroy(impl->stream); + if (impl->core && impl->do_disconnect) + pw_core_disconnect(impl->core); + + pw_properties_free(impl->stream_props); + pw_properties_free(impl->props); + + free(impl); +} + +static void module_destroy(void *data) +{ + struct impl *impl = (struct impl *)data; + spa_hook_remove(&impl->module_listener); + impl_destroy(impl); +} + +static const struct pw_impl_module_events module_events = { + PW_VERSION_IMPL_MODULE_EVENTS, + .destroy = module_destroy, +}; + +static inline uint32_t format_from_name(const char *name, size_t len) +{ + int i; + for (i = 0; spa_type_audio_format[i].name; i++) { + if (strncmp(name, spa_debug_type_short_name(spa_type_audio_format[i].name), len) == 0) + return spa_type_audio_format[i].type; + } + return SPA_AUDIO_FORMAT_UNKNOWN; +} + +static uint32_t channel_from_name(const char *name) +{ + int i; + for (i = 0; spa_type_audio_channel[i].name; i++) { + if (spa_streq(name, spa_debug_type_short_name(spa_type_audio_channel[i].name))) + return spa_type_audio_channel[i].type; + } + return SPA_AUDIO_CHANNEL_UNKNOWN; +} + +static void parse_position(struct spa_audio_info_raw *info, const char *val, size_t len) +{ + struct spa_json it[2]; + char v[256]; + + spa_json_init(&it[0], val, len); + if (spa_json_enter_array(&it[0], &it[1]) <= 0) + spa_json_init(&it[1], val, len); + + info->channels = 0; + while (spa_json_get_string(&it[1], v, sizeof(v)) > 0 && + info->channels < SPA_AUDIO_MAX_CHANNELS) { + info->position[info->channels++] = channel_from_name(v); + } +} + +static void parse_audio_info(const struct pw_properties *props, struct spa_audio_info_raw *info) +{ + const char *str; + + spa_zero(*info); + if ((str = pw_properties_get(props, PW_KEY_AUDIO_FORMAT)) == NULL) + str = DEFAULT_FORMAT; + info->format = static_cast(format_from_name(str, strlen(str))); + switch (info->format) { + case SPA_AUDIO_FORMAT_S16_LE: + case SPA_AUDIO_FORMAT_F32_LE: + break; + default: + pw_log_error( "audio format not supported. fallback to SPA_AUDIO_FORMAT_S16_LE. "); + info->format = SPA_AUDIO_FORMAT_S16_LE; + } + + info->rate = pw_properties_get_uint32(props, PW_KEY_AUDIO_RATE, info->rate); + if (info->rate == 0) + info->rate = DEFAULT_RATE; + + info->channels = pw_properties_get_uint32(props, PW_KEY_AUDIO_CHANNELS, info->channels); + info->channels = SPA_MIN(info->channels, SPA_AUDIO_MAX_CHANNELS); + if ((str = pw_properties_get(props, SPA_KEY_AUDIO_POSITION)) != NULL) + parse_position(info, str, strlen(str)); + if (info->channels == 0) + parse_position(info, DEFAULT_POSITION, strlen(DEFAULT_POSITION)); +} + +static int calc_frame_size(const struct spa_audio_info_raw *info) +{ + int res = info->channels; + switch (info->format) { + case SPA_AUDIO_FORMAT_U8: + case SPA_AUDIO_FORMAT_S8: + case SPA_AUDIO_FORMAT_ALAW: + case SPA_AUDIO_FORMAT_ULAW: + return res; + case SPA_AUDIO_FORMAT_S16: + case SPA_AUDIO_FORMAT_S16_OE: + case SPA_AUDIO_FORMAT_U16: + return res * 2; + case SPA_AUDIO_FORMAT_S24: + case SPA_AUDIO_FORMAT_S24_OE: + case SPA_AUDIO_FORMAT_U24: + return res * 3; + case SPA_AUDIO_FORMAT_S24_32: + case SPA_AUDIO_FORMAT_S24_32_OE: + case SPA_AUDIO_FORMAT_S32: + case SPA_AUDIO_FORMAT_S32_OE: + case SPA_AUDIO_FORMAT_U32: + case SPA_AUDIO_FORMAT_U32_OE: + case SPA_AUDIO_FORMAT_F32: + case SPA_AUDIO_FORMAT_F32_OE: + return res * 4; + case SPA_AUDIO_FORMAT_F64: + case SPA_AUDIO_FORMAT_F64_OE: + return res * 8; + default: + return 0; + } +} + +static void copy_props(struct impl *impl, struct pw_properties *props, const char *key) +{ + const char *str; + if ((str = pw_properties_get(props, key)) != NULL) { + if (pw_properties_get(impl->stream_props, key) == NULL) + pw_properties_set(impl->stream_props, key, str); + } +} + +SPA_EXPORT +int pipewire__module_init(struct pw_impl_module *module, const char *args) +{ + struct pw_context *context = pw_impl_module_get_context(module); + struct pw_properties *props = NULL; + uint32_t id = pw_global_get_id(pw_impl_module_get_global(module)); + uint32_t pid = getpid(); + struct impl *impl; + const char *str; + int res; + spa_dict temp_spa_dict; + + PW_LOG_TOPIC_INIT(mod_topic); + + impl = (struct impl *)calloc(1, sizeof(struct impl)); + if (impl == NULL) + return -errno; + + pw_log_debug("module %p: new %s", impl, args); + + if (args == NULL) + args = ""; + + props = pw_properties_new_string(args); + if (props == NULL) { + res = -errno; + pw_log_error( "can't create properties: %m"); + goto error; + } + impl->props = props; + + impl->stream_props = pw_properties_new(NULL, NULL); + if (impl->stream_props == NULL) { + res = -errno; + pw_log_error( "can't create properties: %m"); + goto error; + } + + impl->module = module; + impl->context = context; + + if (pw_properties_get(props, PW_KEY_NODE_VIRTUAL) == NULL) + pw_properties_set(props, PW_KEY_NODE_VIRTUAL, "true"); + + if (pw_properties_get(props, PW_KEY_MEDIA_CLASS) == NULL) + pw_properties_set(props, PW_KEY_MEDIA_CLASS, "Audio/Sink"); + + if (pw_properties_get(props, PW_KEY_NODE_NAME) == NULL) + pw_properties_setf(props, PW_KEY_NODE_NAME, "oboe-sink-%u-%u", pid, id); + if (pw_properties_get(props, PW_KEY_NODE_DESCRIPTION) == NULL) + pw_properties_set(props, PW_KEY_NODE_DESCRIPTION, + pw_properties_get(props, PW_KEY_NODE_NAME)); + + if ((str = pw_properties_get(props, "stream.props")) != NULL) { + pw_properties_update_string(impl->stream_props, str, strlen(str)); + pw_log_debug( "stream.props set by args: %s", str); + } + + copy_props(impl, props, PW_KEY_AUDIO_RATE); + copy_props(impl, props, PW_KEY_AUDIO_CHANNELS); + copy_props(impl, props, SPA_KEY_AUDIO_POSITION); + copy_props(impl, props, PW_KEY_NODE_NAME); + copy_props(impl, props, PW_KEY_NODE_DESCRIPTION); + copy_props(impl, props, PW_KEY_NODE_GROUP); + copy_props(impl, props, PW_KEY_NODE_LATENCY); + copy_props(impl, props, PW_KEY_NODE_VIRTUAL); + copy_props(impl, props, PW_KEY_MEDIA_CLASS); + + parse_audio_info(impl->stream_props, &impl->info); + + impl->frame_size = calc_frame_size(&impl->info); + impl->stream_write_timeout = pw_properties_get_uint64(props, "stream.write.timeout", DEFAULT_STREAM_WRITE_TIMEOUT); + pw_log_debug( "stream write timeout set to %d", impl->stream_write_timeout); + if (impl->frame_size == 0) { + res = -EINVAL; + pw_log_error( "can't parse audio format"); + goto error; + } + + impl->core = (struct pw_core *)pw_context_get_object(impl->context, PW_TYPE_INTERFACE_Core); + if (impl->core == NULL) { + str = pw_properties_get(props, PW_KEY_REMOTE_NAME); + impl->core = pw_context_connect(impl->context, + pw_properties_new( + PW_KEY_REMOTE_NAME, str, + NULL), + 0); + impl->do_disconnect = true; + } + if (impl->core == NULL) { + res = -errno; + pw_log_error("can't connect: %m"); + goto error; + } + + pw_proxy_add_listener((struct pw_proxy*)impl->core, + &impl->core_proxy_listener, + &core_proxy_events, impl); + pw_core_add_listener(impl->core, + &impl->core_listener, + &core_events, impl); + + if ((res = create_stream(impl)) < 0) + goto error; + + pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl); + + temp_spa_dict = (struct spa_dict) { 0, SPA_N_ELEMENTS(module_props), (module_props) }; + pw_impl_module_update_properties(module, &temp_spa_dict); + + return 0; + +error: + impl_destroy(impl); + return res; +} + + +} diff --git a/packages/pipewire/module-oboe-source.cpp b/packages/pipewire/module-oboe-source.cpp new file mode 100644 index 00000000000000..8defdbac319148 --- /dev/null +++ b/packages/pipewire/module-oboe-source.cpp @@ -0,0 +1,586 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include + +extern "C" { + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#define NANOS_PER_MILLISECOND 1000000L + +/** \page page_module_oboe_source Oboe Source + * + * The oboe source is a good starting point for writing a custom + * source. We refer to the source code for more information. + * + * ## Module Name + * + * `libpipewire-module-oboe-source` + * + * ## Module Options + * + * - `node.name`: a unique name for the stream + * - `node.description`: a human readable name for the stream + * - `stream.props = {}`: properties to be passed to the stream + * + * ## General options + * + * Options with well-known behavior. + * + * - \ref PW_KEY_REMOTE_NAME + * - \ref PW_KEY_AUDIO_FORMAT + * - \ref PW_KEY_AUDIO_RATE + * - \ref PW_KEY_AUDIO_CHANNELS + * - \ref SPA_KEY_AUDIO_POSITION + * - \ref PW_KEY_MEDIA_NAME + * - \ref PW_KEY_NODE_LATENCY + * - \ref PW_KEY_NODE_NAME + * - \ref PW_KEY_NODE_DESCRIPTION + * - \ref PW_KEY_NODE_GROUP + * - \ref PW_KEY_NODE_VIRTUAL + * - \ref PW_KEY_MEDIA_CLASS + * + * ## Oboe configuration + * + *\code{.unparsed} + * context.modules = [ + * { name = libpipewire-module-oboe-source + * args = { + * node.name = "oboe_source" + * node.description = "My Oboe Source" + * stream.props = { + * audio.position = [ FL FR ] + * } + * } + * } + * ] + *\endcode + */ + +#define NAME "oboe-source" + +PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME); +#define PW_LOG_TOPIC_DEFAULT mod_topic + +#define DEFAULT_FORMAT "S16LE" +#define DEFAULT_RATE 48000 +#define DEFAULT_CHANNELS 2 +#define DEFAULT_POSITION "[ FL FR ]" +#define DEFAULT_STREAM_READ_TIMEOUT 0 + +#define MODULE_USAGE "( node.latency= ) " \ + "( node.name= ) " \ + "( node.description= ) " \ + "( audio.format= ) " \ + "( audio.rate= ) " \ + "( audio.channels= ) " \ + "( audio.position= ) " \ + "( stream.read.timeout= ) " \ + "( stream.props= ) " + + + +static const struct spa_dict_item module_props[] = { + { PW_KEY_MODULE_AUTHOR, "Ronald Y" }, + { PW_KEY_MODULE_DESCRIPTION, "Oboe (Andoird) audio source" }, + { PW_KEY_MODULE_USAGE, MODULE_USAGE }, + { PW_KEY_MODULE_VERSION, PACKAGE_VERSION }, +}; + +struct impl { + struct pw_context *context; + + struct pw_properties *props; + + struct pw_impl_module *module; + + struct spa_hook module_listener; + + struct pw_core *core; + struct spa_hook core_proxy_listener; + struct spa_hook core_listener; + + struct pw_properties *stream_props; + struct pw_stream *stream; + struct spa_hook stream_listener; + struct spa_audio_info_raw info; + uint32_t frame_size; + int64_t stream_read_timeout; + + unsigned int do_disconnect:1; + unsigned int unloading:1; + + std::shared_ptr oboe_stream; +}; + +static void stream_destroy(void *d) +{ + struct impl *impl = (struct impl *)d; + spa_hook_remove(&impl->stream_listener); + impl->stream = NULL; +} + +static void stream_state_changed(void *d, enum pw_stream_state old, + enum pw_stream_state state, const char *error) +{ + struct impl *impl = (struct impl *)d; + switch (state) { + case PW_STREAM_STATE_ERROR: + case PW_STREAM_STATE_UNCONNECTED: + pw_log_debug("destroy triggered by stream state changed, pw_stream_state = %d", state); + impl->oboe_stream->close(); + pw_impl_module_schedule_destroy(impl->module); + break; + case PW_STREAM_STATE_PAUSED: + case PW_STREAM_STATE_STREAMING: + break; + default: + break; + } +} + +static int open_oboe_stream(struct impl *impl); + +static void capture_stream_process(void *d) +{ + struct impl *impl = (struct impl *)d; + struct pw_buffer *buf; + struct spa_data *bd; + void *data; + uint32_t size, numFrames, i; + + if ((buf = pw_stream_dequeue_buffer(impl->stream)) == NULL) { + pw_log_debug("out of buffers: %m"); + return; + } + + for (i = 0; i < buf->buffer->n_datas; i++) { + bd = &buf->buffer->datas[i]; + + data = bd->data; + size = buf->requested ? buf->requested * impl->frame_size : bd->maxsize; + numFrames = size / impl->frame_size; + + auto returnCode = impl->oboe_stream->read(data, numFrames, impl->stream_read_timeout); + if (returnCode == oboe::Result::OK) { + if (impl->stream_read_timeout == 0) { + size = returnCode.value() * impl->frame_size; + if (returnCode.value() != numFrames) + pw_log_debug("number of frames read: %d", returnCode.value()); + } + } else { + if (returnCode == oboe::Result::ErrorDisconnected) + open_oboe_stream(impl); + else + pw_log_error("Oboe stream read() error: %s", oboe::convertToText(oboe::Result(returnCode))); + } + pw_log_info("fill buffer data %p with up to %u bytes", data, size); + + bd->chunk->size = size; + bd->chunk->stride = impl->frame_size; + bd->chunk->offset = 0; + buf->size = size / impl->frame_size; + } + + pw_stream_queue_buffer(impl->stream, buf); +} + +static const struct pw_stream_events capture_stream_events = { + PW_VERSION_STREAM_EVENTS, + .destroy = stream_destroy, + .state_changed = stream_state_changed, + .process = capture_stream_process +}; + +// static void core_destroy(void *d); + +// static void error_callback(std::shared_ptr stream, void *impl, oboe::Result error) { +// pw_log_debug("Oboe error: %d", error); +// core_destroy(impl); +// } + +#define CHK(stmt) { \ + oboe::Result res = stmt; \ + if (res != oboe::Result::OK) { \ + pw_log_error("Oboe error %s at %s:%d\n", oboe::convertToText(res), __FILE__, __LINE__); \ + goto fail; \ + } \ +} + +static int open_oboe_stream(struct impl *impl) +{ + oboe::AudioFormat format; + oboe::AudioStreamBuilder builder; + switch (impl->info.format) { + case SPA_AUDIO_FORMAT_S16_LE: format = oboe::AudioFormat::I16; break; + case SPA_AUDIO_FORMAT_F32_LE: format = oboe::AudioFormat::Float; break; + default: + pw_log_error( "audio format not supported. "); + goto fail; + } + + CHK(builder.setDirection(oboe::Direction::Input) + ->setSharingMode(oboe::SharingMode::Shared) + ->setInputPreset(oboe::InputPreset::Unprocessed) + ->setChannelCount(impl->info.channels) + ->setSampleRate(impl->info.rate) + ->setSampleRateConversionQuality(oboe::SampleRateConversionQuality::Medium) + ->setFormat(format) + // TODO: error callback + // ->setErrorCallback(error_callback) + ->openStream(impl->oboe_stream)); + + impl->info.rate = impl->oboe_stream->getSampleRate(); + + CHK(impl->oboe_stream->requestStart()); + CHK(impl->oboe_stream->waitForStateChange(oboe::StreamState::Starting, NULL, 1000 * NANOS_PER_MILLISECOND)); + return 0; + +fail: + return -1; +} + +static int create_stream(struct impl *impl) +{ + int res; + uint32_t n_params; + const struct spa_pod *params[1]; + uint8_t buffer[1024]; + struct spa_pod_builder b; + + open_oboe_stream(impl); + impl->stream = pw_stream_new(impl->core, "oboe source", impl->stream_props); + impl->stream_props = NULL; + + if (impl->stream == NULL) + return -errno; + + pw_stream_add_listener(impl->stream, + &impl->stream_listener, + &capture_stream_events, impl); + + n_params = 0; + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + params[n_params++] = spa_format_audio_raw_build(&b, + SPA_PARAM_EnumFormat, &impl->info); + + if ((res = pw_stream_connect(impl->stream, + PW_DIRECTION_OUTPUT, + PW_ID_ANY, + static_cast(PW_STREAM_FLAG_AUTOCONNECT | + PW_STREAM_FLAG_MAP_BUFFERS | + PW_STREAM_FLAG_RT_PROCESS), + params, n_params)) < 0) + return res; + + return 0; +} + +static void core_error(void *data, uint32_t id, int seq, int res, const char *message) +{ + struct impl *impl = (struct impl *)data; + + pw_log_error("error id:%u seq:%d res:%d (%s): %s", + id, seq, res, spa_strerror(res), message); + + if (id == PW_ID_CORE && res == -EPIPE) { + impl->oboe_stream->close(); + pw_impl_module_schedule_destroy(impl->module); + } +} + +static const struct pw_core_events core_events = { + PW_VERSION_CORE_EVENTS, + .error = core_error, +}; + +static void core_destroy(void *d) +{ + struct impl *impl = (struct impl *)d; + impl->oboe_stream->close(); + spa_hook_remove(&impl->core_listener); + impl->core = NULL; + pw_impl_module_schedule_destroy(impl->module); +} + +static const struct pw_proxy_events core_proxy_events = { + .destroy = core_destroy, +}; + +static void impl_destroy(struct impl *impl) +{ + if (impl->stream) + pw_stream_destroy(impl->stream); + if (impl->core && impl->do_disconnect) + pw_core_disconnect(impl->core); + + pw_properties_free(impl->stream_props); + pw_properties_free(impl->props); + + free(impl); +} + +static void module_destroy(void *data) +{ + struct impl *impl = (struct impl *)data; + impl->unloading = true; + spa_hook_remove(&impl->module_listener); + impl_destroy(impl); +} + +static const struct pw_impl_module_events module_events = { + PW_VERSION_IMPL_MODULE_EVENTS, + .destroy = module_destroy, +}; + +static inline uint32_t format_from_name(const char *name, size_t len) +{ + int i; + for (i = 0; spa_type_audio_format[i].name; i++) { + if (strncmp(name, spa_debug_type_short_name(spa_type_audio_format[i].name), len) == 0) + return spa_type_audio_format[i].type; + } + return SPA_AUDIO_FORMAT_UNKNOWN; +} + +static uint32_t channel_from_name(const char *name) +{ + int i; + for (i = 0; spa_type_audio_channel[i].name; i++) { + if (spa_streq(name, spa_debug_type_short_name(spa_type_audio_channel[i].name))) + return spa_type_audio_channel[i].type; + } + return SPA_AUDIO_CHANNEL_UNKNOWN; +} + +static void parse_position(struct spa_audio_info_raw *info, const char *val, size_t len) +{ + struct spa_json it[2]; + char v[256]; + + spa_json_init(&it[0], val, len); + if (spa_json_enter_array(&it[0], &it[1]) <= 0) + spa_json_init(&it[1], val, len); + + info->channels = 0; + while (spa_json_get_string(&it[1], v, sizeof(v)) > 0 && + info->channels < SPA_AUDIO_MAX_CHANNELS) { + info->position[info->channels++] = channel_from_name(v); + } +} + +static void parse_audio_info(const struct pw_properties *props, struct spa_audio_info_raw *info) +{ + const char *str; + + spa_zero(*info); + if ((str = pw_properties_get(props, PW_KEY_AUDIO_FORMAT)) == NULL) + str = DEFAULT_FORMAT; + info->format = static_cast(format_from_name(str, strlen(str))); + switch (info->format) { + case SPA_AUDIO_FORMAT_S16_LE: + case SPA_AUDIO_FORMAT_F32_LE: + break; + default: + pw_log_error( "audio format not supported. fallback to SPA_AUDIO_FORMAT_S16_LE. "); + info->format = SPA_AUDIO_FORMAT_S16_LE; + } + + info->rate = pw_properties_get_uint32(props, PW_KEY_AUDIO_RATE, info->rate); + if (info->rate == 0) + info->rate = DEFAULT_RATE; + + info->channels = pw_properties_get_uint32(props, PW_KEY_AUDIO_CHANNELS, info->channels); + info->channels = SPA_MIN(info->channels, SPA_AUDIO_MAX_CHANNELS); + if ((str = pw_properties_get(props, SPA_KEY_AUDIO_POSITION)) != NULL) + parse_position(info, str, strlen(str)); + if (info->channels == 0) + parse_position(info, DEFAULT_POSITION, strlen(DEFAULT_POSITION)); +} + +static int calc_frame_size(const struct spa_audio_info_raw *info) +{ + int res = info->channels; + switch (info->format) { + case SPA_AUDIO_FORMAT_U8: + case SPA_AUDIO_FORMAT_S8: + case SPA_AUDIO_FORMAT_ALAW: + case SPA_AUDIO_FORMAT_ULAW: + return res; + case SPA_AUDIO_FORMAT_S16: + case SPA_AUDIO_FORMAT_S16_OE: + case SPA_AUDIO_FORMAT_U16: + return res * 2; + case SPA_AUDIO_FORMAT_S24: + case SPA_AUDIO_FORMAT_S24_OE: + case SPA_AUDIO_FORMAT_U24: + return res * 3; + case SPA_AUDIO_FORMAT_S24_32: + case SPA_AUDIO_FORMAT_S24_32_OE: + case SPA_AUDIO_FORMAT_S32: + case SPA_AUDIO_FORMAT_S32_OE: + case SPA_AUDIO_FORMAT_U32: + case SPA_AUDIO_FORMAT_U32_OE: + case SPA_AUDIO_FORMAT_F32: + case SPA_AUDIO_FORMAT_F32_OE: + return res * 4; + case SPA_AUDIO_FORMAT_F64: + case SPA_AUDIO_FORMAT_F64_OE: + return res * 8; + default: + return 0; + } +} + +static void copy_props(struct impl *impl, struct pw_properties *props, const char *key) +{ + const char *str; + if ((str = pw_properties_get(props, key)) != NULL) { + if (pw_properties_get(impl->stream_props, key) == NULL) + pw_properties_set(impl->stream_props, key, str); + } +} + +SPA_EXPORT +int pipewire__module_init(struct pw_impl_module *module, const char *args) +{ + struct pw_context *context = pw_impl_module_get_context(module); + struct pw_properties *props = NULL; + uint32_t id = pw_global_get_id(pw_impl_module_get_global(module)); + uint32_t pid = getpid(); + struct impl *impl; + const char *str; + int res; + spa_dict temp_spa_dict; + + PW_LOG_TOPIC_INIT(mod_topic); + + impl = (struct impl *)calloc(1, sizeof(struct impl)); + if (impl == NULL) + return -errno; + + pw_log_debug("module %p: new %s", impl, args); + + if (args == NULL) + args = ""; + + props = pw_properties_new_string(args); + if (props == NULL) { + res = -errno; + pw_log_error( "can't create properties: %m"); + goto error; + } + impl->props = props; + + impl->stream_props = pw_properties_new(NULL, NULL); + if (impl->stream_props == NULL) { + res = -errno; + pw_log_error( "can't create properties: %m"); + goto error; + } + + impl->module = module; + impl->context = context; + + if (pw_properties_get(props, PW_KEY_NODE_VIRTUAL) == NULL) + pw_properties_set(props, PW_KEY_NODE_VIRTUAL, "true"); + + if (pw_properties_get(props, PW_KEY_MEDIA_CLASS) == NULL) + pw_properties_set(props, PW_KEY_MEDIA_CLASS, "Audio/Source"); + + if (pw_properties_get(props, PW_KEY_NODE_NAME) == NULL) + pw_properties_setf(props, PW_KEY_NODE_NAME, "oboe-source-%u-%u", pid, id); + if (pw_properties_get(props, PW_KEY_NODE_DESCRIPTION) == NULL) + pw_properties_set(props, PW_KEY_NODE_DESCRIPTION, + pw_properties_get(props, PW_KEY_NODE_NAME)); + + if ((str = pw_properties_get(props, "stream.props")) != NULL) + pw_properties_update_string(impl->stream_props, str, strlen(str)); + + copy_props(impl, props, PW_KEY_AUDIO_RATE); + copy_props(impl, props, PW_KEY_AUDIO_CHANNELS); + copy_props(impl, props, SPA_KEY_AUDIO_POSITION); + copy_props(impl, props, PW_KEY_NODE_NAME); + copy_props(impl, props, PW_KEY_NODE_DESCRIPTION); + copy_props(impl, props, PW_KEY_NODE_GROUP); + copy_props(impl, props, PW_KEY_NODE_LATENCY); + copy_props(impl, props, PW_KEY_NODE_VIRTUAL); + copy_props(impl, props, PW_KEY_MEDIA_CLASS); + + parse_audio_info(impl->stream_props, &impl->info); + + impl->frame_size = calc_frame_size(&impl->info); + + impl->stream_read_timeout = pw_properties_get_uint64(impl->stream_props, "stream.read.timeout", DEFAULT_STREAM_READ_TIMEOUT); + pw_log_debug( "stream read timeout set to %d", impl->stream_read_timeout); + if (impl->frame_size == 0) { + res = -EINVAL; + pw_log_error( "can't parse audio format"); + goto error; + } + + impl->core = (struct pw_core *)pw_context_get_object(impl->context, PW_TYPE_INTERFACE_Core); + if (impl->core == NULL) { + str = pw_properties_get(props, PW_KEY_REMOTE_NAME); + impl->core = pw_context_connect(impl->context, + pw_properties_new( + PW_KEY_REMOTE_NAME, str, + NULL), + 0); + impl->do_disconnect = true; + } + if (impl->core == NULL) { + res = -errno; + pw_log_error("can't connect: %m"); + goto error; + } + + pw_proxy_add_listener((struct pw_proxy*)impl->core, + &impl->core_proxy_listener, + &core_proxy_events, impl); + pw_core_add_listener(impl->core, + &impl->core_listener, + &core_events, impl); + + if ((res = create_stream(impl)) < 0) + goto error; + + pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl); + + temp_spa_dict = (struct spa_dict) { 0, SPA_N_ELEMENTS(module_props), (module_props) }; + pw_impl_module_update_properties(module, &temp_spa_dict); + + return 0; + +error: + impl_destroy(impl); + return res; +} + +} diff --git a/packages/pipewire/module-protocol-pulse/module-aaudio-sink.c b/packages/pipewire/module-protocol-pulse/module-aaudio-sink.c new file mode 100644 index 00000000000000..99e1ad7a3d8533 --- /dev/null +++ b/packages/pipewire/module-protocol-pulse/module-aaudio-sink.c @@ -0,0 +1,203 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include + +#include "../defs.h" +#include "../module.h" + +/** \page page_pulse_module_aaudio_sink AAudio Sink + * + * ## Module Name + * + * `module-aaudio-sink` + * + * ## Module Options + * + * @pulse_module_options@ + * + * ## See Also + * + * \ref page_module_aaudio_sink "libpipewire-module-aaudio-sink" + */ + +static const char *const pulse_module_options = + "node_latency= " + "node_name= " + "node_description= " + "audio_format= " + "audio_rate= " + "audio_channels= " + "audio_position= " + "stream_props= " + ; + +#define NAME "aaudio-sink" + +PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME); +#define PW_LOG_TOPIC_DEFAULT mod_topic + +struct module_aaudio_sink_data { + struct module *module; + + struct spa_hook mod_listener; + struct pw_impl_module *mod; + + struct pw_properties *stream_props; + struct pw_properties *aaudio_props; +}; + +static void module_destroy(void *data) +{ + struct module_aaudio_sink_data *d = data; + spa_hook_remove(&d->mod_listener); + d->mod = NULL; + module_schedule_unload(d->module); +} + +static const struct pw_impl_module_events module_events = { + PW_VERSION_IMPL_MODULE_EVENTS, + .destroy = module_destroy +}; + +static int module_aaudio_sink_load(struct module *module) +{ + struct module_aaudio_sink_data *data = module->user_data; + FILE *f; + char *args; + size_t size; + + if ((f = open_memstream(&args, &size)) == NULL) + return -errno; + + fprintf(f, "{"); + pw_properties_serialize_dict(f, &data->aaudio_props->dict, 0); + fprintf(f, " stream.props = {"); + pw_properties_serialize_dict(f, &data->stream_props->dict, 0); + fprintf(f, " } }"); + fclose(f); + + data->mod = pw_context_load_module(module->impl->context, + "libpipewire-module-aaudio-sink", + args, NULL); + + free(args); + + if (data->mod == NULL) + return -errno; + + pw_impl_module_add_listener(data->mod, + &data->mod_listener, + &module_events, data); + + return 0; +} + +static int module_aaudio_sink_unload(struct module *module) +{ + struct module_aaudio_sink_data *d = module->user_data; + + if (d->mod) { + spa_hook_remove(&d->mod_listener); + pw_impl_module_destroy(d->mod); + d->mod = NULL; + } + + pw_properties_free(d->aaudio_props); + pw_properties_free(d->stream_props); + + return 0; +} + +static const char* const valid_args[] = { + "node_latency", + "node_name", + "node_description", + "audio_format", + "audio_rate", + "audio_channels", + "audio_position", + "stream_write_timeout", + "stream_props", + NULL +}; +static const struct spa_dict_item module_aaudio_sink_info[] = { + { PW_KEY_MODULE_AUTHOR, "Ronald Y" }, + { PW_KEY_MODULE_DESCRIPTION, "AAudio (Andoird) audio sink" }, + { PW_KEY_MODULE_USAGE, pulse_module_options }, + { PW_KEY_MODULE_VERSION, PACKAGE_VERSION }, +}; + +static int module_aaudio_sink_prepare(struct module * const module) +{ + struct module_aaudio_sink_data * const d = module->user_data; + struct pw_properties * const props = module->props; + struct pw_properties *stream_props = NULL, *aaudio_props = NULL; + const char *str; + int res; + + PW_LOG_TOPIC_INIT(mod_topic); + + stream_props = pw_properties_new(NULL, NULL); + aaudio_props = pw_properties_new(NULL, NULL); + if (!stream_props || !aaudio_props) { + res = -errno; + goto out; + } + + if ((str = pw_properties_get(props, "node_latency")) != NULL) { + pw_properties_set(aaudio_props, "node.latency", str); + pw_properties_set(props, "node_latency", NULL); + } + if ((str = pw_properties_get(props, "node_name")) != NULL) { + pw_properties_set(aaudio_props, "node.name", str); + pw_properties_set(props, "node_name", NULL); + } + if ((str = pw_properties_get(props, "node_description")) != NULL) { + pw_properties_set(aaudio_props, "node.description", str); + pw_properties_set(props, "node_description", NULL); + } + if ((str = pw_properties_get(props, "audio_format")) != NULL) { + pw_properties_set(aaudio_props, "audio.format", str); + pw_properties_set(props, "audio_format", NULL); + } + if ((str = pw_properties_get(props, "audio_rate")) != NULL) { + pw_properties_set(aaudio_props, "audio.rate", str); + pw_properties_set(props, "audio_rate", NULL); + } + if ((str = pw_properties_get(props, "audio_channels")) != NULL) { + pw_properties_set(aaudio_props, "audio.channels", str); + pw_properties_set(props, "audio_channels", NULL); + } + if ((str = pw_properties_get(props, "audio_position")) != NULL) { + pw_properties_set(aaudio_props, "audio.position", str); + pw_properties_set(props, "audio_position", NULL); + } + if ((str = pw_properties_get(props, "stream_properties")) != NULL) { + module_args_add_props(stream_props, str); + pw_properties_set(props, "stream_properties", NULL); + } + + d->module = module; + d->stream_props = stream_props; + d->aaudio_props = aaudio_props; + + return 0; +out: + pw_properties_free(stream_props); + pw_properties_free(aaudio_props); + + return res; +} + +DEFINE_MODULE_INFO(module_aaudio_sink) = { + .name = "module-aaudio-sink", + .valid_args = valid_args, + .prepare = module_aaudio_sink_prepare, + .load = module_aaudio_sink_load, + .unload = module_aaudio_sink_unload, + .properties = &SPA_DICT_INIT_ARRAY(module_aaudio_sink_info), + .data_size = sizeof(struct module_aaudio_sink_data), +}; diff --git a/packages/pipewire/module-protocol-pulse/module-aaudio-source.c b/packages/pipewire/module-protocol-pulse/module-aaudio-source.c new file mode 100644 index 00000000000000..fb311966ab2272 --- /dev/null +++ b/packages/pipewire/module-protocol-pulse/module-aaudio-source.c @@ -0,0 +1,222 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ +/* SPDX-FileCopyrightText: Copyright © 2021 Sanchayan Maity */ +/* SPDX-License-Identifier: MIT */ + +#include +#include + +#include "../defs.h" +#include "../module.h" + +/** \page page_pulse_module_aaudio_source AAudio Source + * + * ## Module Name + * + * `module-aaudio-source` + * + * ## Module Options + * + * @pulse_module_options@ + * + * ## See Also + * + * \ref page_module_aaudio_source "libpipewire-module-aaudio-source" + */ + +static const char *const pulse_module_options = + "source_name= " + "source_properties= " + "resampler_profile=|high|medium|low " + "fec_code=|disable|rs8m|ldpc " + "sess_latency_msec= " + "local_ip= " + "local_source_port= " + "local_repair_port= " + "local_control_port= " + ; + +#define NAME "aaudio-source" + +PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME); +#define PW_LOG_TOPIC_DEFAULT mod_topic + +struct module_aaudio_source_data { + struct module *module; + + struct spa_hook mod_listener; + struct pw_impl_module *mod; + + struct pw_properties *stream_props; + struct pw_properties *aaudio_props; +}; + +static void module_destroy(void *data) +{ + struct module_aaudio_source_data *d = data; + spa_hook_remove(&d->mod_listener); + d->mod = NULL; + module_schedule_unload(d->module); +} + +static const struct pw_impl_module_events module_events = { + PW_VERSION_IMPL_MODULE_EVENTS, + .destroy = module_destroy +}; + +static int module_aaudio_source_load(struct module *module) +{ + struct module_aaudio_source_data *data = module->user_data; + FILE *f; + char *args; + size_t size; + + if ((f = open_memstream(&args, &size)) == NULL) + return -errno; + + fprintf(f, "{"); + pw_properties_serialize_dict(f, &data->aaudio_props->dict, 0); + fprintf(f, " stream.props = {"); + pw_properties_serialize_dict(f, &data->stream_props->dict, 0); + fprintf(f, " } }"); + fclose(f); + + data->mod = pw_context_load_module(module->impl->context, + "libpipewire-module-aaudio-source", + args, NULL); + + free(args); + + if (data->mod == NULL) + return -errno; + + pw_impl_module_add_listener(data->mod, + &data->mod_listener, + &module_events, data); + + return 0; +} + +static int module_aaudio_source_unload(struct module *module) +{ + struct module_aaudio_source_data *d = module->user_data; + + if (d->mod) { + spa_hook_remove(&d->mod_listener); + pw_impl_module_destroy(d->mod); + d->mod = NULL; + } + + pw_properties_free(d->aaudio_props); + pw_properties_free(d->stream_props); + + return 0; +} + +static const char* const valid_args[] = { + "source_name", + "source_properties", + "resampler_profile", + "fec_code", + "sess_latency_msec", + "local_ip", + "local_source_port", + "local_repair_port", + "local_control_port", + NULL +}; + +static const struct spa_dict_item module_aaudio_source_info[] = { + { PW_KEY_MODULE_AUTHOR, "Sanchayan Maity " }, + { PW_KEY_MODULE_DESCRIPTION, "aaudio source" }, + { PW_KEY_MODULE_USAGE, pulse_module_options }, + { PW_KEY_MODULE_VERSION, PACKAGE_VERSION }, +}; + +static int module_aaudio_source_prepare(struct module * const module) +{ + struct module_aaudio_source_data * const d = module->user_data; + struct pw_properties * const props = module->props; + struct pw_properties *stream_props = NULL, *aaudio_props = NULL; + const char *str; + int res; + + PW_LOG_TOPIC_INIT(mod_topic); + + stream_props = pw_properties_new(NULL, NULL); + aaudio_props = pw_properties_new(NULL, NULL); + if (!stream_props || !aaudio_props) { + res = -errno; + goto out; + } + + if ((str = pw_properties_get(props, "source_name")) != NULL) { + pw_properties_set(stream_props, PW_KEY_NODE_NAME, str); + pw_properties_set(props, "source_name", NULL); + } + if ((str = pw_properties_get(props, "source_properties")) != NULL) { + module_args_add_props(stream_props, str); + pw_properties_set(props, "source_properties", NULL); + } + + if ((str = pw_properties_get(props, PW_KEY_MEDIA_CLASS)) == NULL) { + pw_properties_set(props, PW_KEY_MEDIA_CLASS, "Audio/Source"); + pw_properties_set(stream_props, PW_KEY_MEDIA_CLASS, "Audio/Source"); + } + + if ((str = pw_properties_get(props, "local_ip")) != NULL) { + pw_properties_set(aaudio_props, "local.ip", str); + pw_properties_set(props, "local_ip", NULL); + } + + if ((str = pw_properties_get(props, "local_source_port")) != NULL) { + pw_properties_set(aaudio_props, "local.source.port", str); + pw_properties_set(props, "local_source_port", NULL); + } + + if ((str = pw_properties_get(props, "local_repair_port")) != NULL) { + pw_properties_set(aaudio_props, "local.repair.port", str); + pw_properties_set(props, "local_repair_port", NULL); + } + + if ((str = pw_properties_get(props, "local_control_port")) != NULL) { + pw_properties_set(aaudio_props, "local.control.port", str); + pw_properties_set(props, "local_control_port", NULL); + } + + if ((str = pw_properties_get(props, "sess_latency_msec")) != NULL) { + pw_properties_set(aaudio_props, "sess.latency.msec", str); + pw_properties_set(props, "sess_latency_msec", NULL); + } + + if ((str = pw_properties_get(props, "resampler_profile")) != NULL) { + pw_properties_set(aaudio_props, "resampler.profile", str); + pw_properties_set(props, "resampler_profile", NULL); + } + + if ((str = pw_properties_get(props, "fec_code")) != NULL) { + pw_properties_set(aaudio_props, "fec.code", str); + pw_properties_set(props, "fec_code", NULL); + } + + d->module = module; + d->stream_props = stream_props; + d->aaudio_props = aaudio_props; + + return 0; +out: + pw_properties_free(stream_props); + pw_properties_free(aaudio_props); + + return res; +} + +DEFINE_MODULE_INFO(module_aaudio_source) = { + .name = "module-aaudio-source", + .valid_args = valid_args, + .prepare = module_aaudio_source_prepare, + .load = module_aaudio_source_load, + .unload = module_aaudio_source_unload, + .properties = &SPA_DICT_INIT_ARRAY(module_aaudio_source_info), + .data_size = sizeof(struct module_aaudio_source_data), +}; diff --git a/packages/pipewire/module-protocol-pulse/module-oboe-sink.c b/packages/pipewire/module-protocol-pulse/module-oboe-sink.c new file mode 100644 index 00000000000000..8e18809795ae98 --- /dev/null +++ b/packages/pipewire/module-protocol-pulse/module-oboe-sink.c @@ -0,0 +1,208 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include + +#include "../defs.h" +#include "../module.h" + +/** \page page_pulse_module_oboe_sink Oboe Sink + * + * ## Module Name + * + * `module-oboe-sink` + * + * ## Module Options + * + * @pulse_module_options@ + * + * ## See Also + * + * \ref page_module_oboe_sink "libpipewire-module-oboe-sink" + */ + +static const char *const pulse_module_options = + "node_latency= " + "node_name= " + "node_description= " + "audio_format= " + "audio_rate= " + "audio_channels= " + "audio_position= " + "stream_write_timeout= " + "stream_props= " + ; + +#define NAME "oboe-sink" + +PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME); +#define PW_LOG_TOPIC_DEFAULT mod_topic + +struct module_oboe_sink_data { + struct module *module; + + struct spa_hook mod_listener; + struct pw_impl_module *mod; + + struct pw_properties *stream_props; + struct pw_properties *oboe_props; +}; + +static void module_destroy(void *data) +{ + struct module_oboe_sink_data *d = data; + spa_hook_remove(&d->mod_listener); + d->mod = NULL; + module_schedule_unload(d->module); +} + +static const struct pw_impl_module_events module_events = { + PW_VERSION_IMPL_MODULE_EVENTS, + .destroy = module_destroy +}; + +static int module_oboe_sink_load(struct module *module) +{ + struct module_oboe_sink_data *data = module->user_data; + FILE *f; + char *args; + size_t size; + + if ((f = open_memstream(&args, &size)) == NULL) + return -errno; + + fprintf(f, "{"); + pw_properties_serialize_dict(f, &data->oboe_props->dict, 0); + fprintf(f, " \"stream.props\": {"); + pw_properties_serialize_dict(f, &data->stream_props->dict, 0); + fprintf(f, " } }"); + fclose(f); + + data->mod = pw_context_load_module(module->impl->context, + "libpipewire-module-oboe-sink", + args, NULL); + + free(args); + + if (data->mod == NULL) + return -errno; + + pw_impl_module_add_listener(data->mod, + &data->mod_listener, + &module_events, data); + + return 0; +} + +static int module_oboe_sink_unload(struct module *module) +{ + struct module_oboe_sink_data *d = module->user_data; + + if (d->mod) { + spa_hook_remove(&d->mod_listener); + pw_impl_module_destroy(d->mod); + d->mod = NULL; + } + + pw_properties_free(d->oboe_props); + pw_properties_free(d->stream_props); + + return 0; +} + +static const char* const valid_args[] = { + "node_latency", + "node_name", + "node_description", + "audio_format", + "audio_rate", + "audio_channels", + "audio_position", + "stream_write_timeout", + "stream_props", + NULL +}; +static const struct spa_dict_item module_oboe_sink_info[] = { + { PW_KEY_MODULE_AUTHOR, "Ronald Y" }, + { PW_KEY_MODULE_DESCRIPTION, "Oboe (Andoird) audio sink" }, + { PW_KEY_MODULE_USAGE, pulse_module_options }, + { PW_KEY_MODULE_VERSION, PACKAGE_VERSION }, +}; + +static int module_oboe_sink_prepare(struct module * const module) +{ + struct module_oboe_sink_data * const d = module->user_data; + struct pw_properties * const props = module->props; + struct pw_properties *stream_props = NULL, *oboe_props = NULL; + const char *str; + int res; + + PW_LOG_TOPIC_INIT(mod_topic); + + stream_props = pw_properties_new(NULL, NULL); + oboe_props = pw_properties_new(NULL, NULL); + if (!stream_props || !oboe_props) { + res = -errno; + goto out; + } + + if ((str = pw_properties_get(props, "node_latency")) != NULL) { + pw_properties_set(oboe_props, "node.latency", str); + pw_properties_set(props, "node_latency", NULL); + } + if ((str = pw_properties_get(props, "node_name")) != NULL) { + pw_properties_set(oboe_props, "node.name", str); + pw_properties_set(props, "node_name", NULL); + } + if ((str = pw_properties_get(props, "node_description")) != NULL) { + pw_properties_set(oboe_props, "node.description", str); + pw_properties_set(props, "node_description", NULL); + } + if ((str = pw_properties_get(props, "audio_format")) != NULL) { + pw_properties_set(oboe_props, "audio.format", str); + pw_properties_set(props, "audio_format", NULL); + } + if ((str = pw_properties_get(props, "audio_rate")) != NULL) { + pw_properties_set(oboe_props, "audio.rate", str); + pw_properties_set(props, "audio_rate", NULL); + } + if ((str = pw_properties_get(props, "audio_channels")) != NULL) { + pw_properties_set(oboe_props, "audio.channels", str); + pw_properties_set(props, "audio_channels", NULL); + } + if ((str = pw_properties_get(props, "audio_position")) != NULL) { + pw_properties_set(oboe_props, "audio.position", str); + pw_properties_set(props, "audio_position", NULL); + } + if ((str = pw_properties_get(props, "stream_write_timeout")) != NULL) { + pw_properties_set(oboe_props, "stream.write.timeout", str); + pw_properties_set(props, "stream_write_timeout", NULL); + } + if ((str = pw_properties_get(props, "stream_properties")) != NULL) { + module_args_add_props(stream_props, str); + pw_properties_set(props, "stream_properties", NULL); + } + + d->module = module; + d->stream_props = stream_props; + d->oboe_props = oboe_props; + + return 0; +out: + pw_properties_free(stream_props); + pw_properties_free(oboe_props); + + return res; +} + +DEFINE_MODULE_INFO(module_oboe_sink) = { + .name = "module-oboe-sink", + .valid_args = valid_args, + .prepare = module_oboe_sink_prepare, + .load = module_oboe_sink_load, + .unload = module_oboe_sink_unload, + .properties = &SPA_DICT_INIT_ARRAY(module_oboe_sink_info), + .data_size = sizeof(struct module_oboe_sink_data), +}; diff --git a/packages/pipewire/module-protocol-pulse/module-oboe-source.c b/packages/pipewire/module-protocol-pulse/module-oboe-source.c new file mode 100644 index 00000000000000..a31ab15d90b061 --- /dev/null +++ b/packages/pipewire/module-protocol-pulse/module-oboe-source.c @@ -0,0 +1,210 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ +/* SPDX-FileCopyrightText: Copyright © 2021 Sanchayan Maity */ +/* SPDX-License-Identifier: MIT */ + +#include +#include + +#include "../defs.h" +#include "../module.h" + +/** \page page_pulse_module_oboe_source Oboe Source + * + * ## Module Name + * + * `module-oboe-source` + * + * ## Module Options + * + * @pulse_module_options@ + * + * ## See Also + * + * \ref page_module_oboe_source "libpipewire-module-oboe-source" + */ + +static const char *const pulse_module_options = + "node_latency= " + "node_name= " + "node_description= " + "audio_format= " + "audio_rate= " + "audio_channels= " + "audio_position= " + "stream_read_timeout= " + "stream_props= " + ; + +#define NAME "oboe-source" + +PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME); +#define PW_LOG_TOPIC_DEFAULT mod_topic + +struct module_oboe_source_data { + struct module *module; + + struct spa_hook mod_listener; + struct pw_impl_module *mod; + + struct pw_properties *stream_props; + struct pw_properties *oboe_props; +}; + +static void module_destroy(void *data) +{ + struct module_oboe_source_data *d = data; + spa_hook_remove(&d->mod_listener); + d->mod = NULL; + module_schedule_unload(d->module); +} + +static const struct pw_impl_module_events module_events = { + PW_VERSION_IMPL_MODULE_EVENTS, + .destroy = module_destroy +}; + +static int module_oboe_source_load(struct module *module) +{ + struct module_oboe_source_data *data = module->user_data; + FILE *f; + char *args; + size_t size; + + if ((f = open_memstream(&args, &size)) == NULL) + return -errno; + + fprintf(f, "{"); + pw_properties_serialize_dict(f, &data->oboe_props->dict, 0); + fprintf(f, " \"stream.props\": {"); + pw_properties_serialize_dict(f, &data->stream_props->dict, 0); + fprintf(f, " } }"); + fclose(f); + + data->mod = pw_context_load_module(module->impl->context, + "libpipewire-module-oboe-source", + args, NULL); + + free(args); + + if (data->mod == NULL) + return -errno; + + pw_impl_module_add_listener(data->mod, + &data->mod_listener, + &module_events, data); + + return 0; +} + +static int module_oboe_source_unload(struct module *module) +{ + struct module_oboe_source_data *d = module->user_data; + + if (d->mod) { + spa_hook_remove(&d->mod_listener); + pw_impl_module_destroy(d->mod); + d->mod = NULL; + } + + pw_properties_free(d->oboe_props); + pw_properties_free(d->stream_props); + + return 0; +} + +static const char* const valid_args[] = { + "node_latency", + "node_name", + "node_description", + "audio_format", + "audio_rate", + "audio_channels", + "audio_position", + "stream_read_timeout", + "stream_props", + NULL +}; + +static const struct spa_dict_item module_oboe_source_info[] = { + { PW_KEY_MODULE_AUTHOR, "Sanchayan Maity " }, + { PW_KEY_MODULE_DESCRIPTION, "oboe source" }, + { PW_KEY_MODULE_USAGE, pulse_module_options }, + { PW_KEY_MODULE_VERSION, PACKAGE_VERSION }, +}; + +static int module_oboe_source_prepare(struct module * const module) +{ + struct module_oboe_source_data * const d = module->user_data; + struct pw_properties * const props = module->props; + struct pw_properties *stream_props = NULL, *oboe_props = NULL; + const char *str; + int res; + + PW_LOG_TOPIC_INIT(mod_topic); + + stream_props = pw_properties_new(NULL, NULL); + oboe_props = pw_properties_new(NULL, NULL); + if (!stream_props || !oboe_props) { + res = -errno; + goto out; + } + + if ((str = pw_properties_get(props, "node_latency")) != NULL) { + pw_properties_set(oboe_props, "node.latency", str); + pw_properties_set(props, "node_latency", NULL); + } + if ((str = pw_properties_get(props, "node_name")) != NULL) { + pw_properties_set(oboe_props, "node.name", str); + pw_properties_set(props, "node_name", NULL); + } + if ((str = pw_properties_get(props, "node_description")) != NULL) { + pw_properties_set(oboe_props, "node.description", str); + pw_properties_set(props, "node_description", NULL); + } + if ((str = pw_properties_get(props, "audio_format")) != NULL) { + pw_properties_set(oboe_props, "audio.format", str); + pw_properties_set(props, "audio_format", NULL); + } + if ((str = pw_properties_get(props, "audio_rate")) != NULL) { + pw_properties_set(oboe_props, "audio.rate", str); + pw_properties_set(props, "audio_rate", NULL); + } + if ((str = pw_properties_get(props, "audio_channels")) != NULL) { + pw_properties_set(oboe_props, "audio.channels", str); + pw_properties_set(props, "audio_channels", NULL); + } + if ((str = pw_properties_get(props, "audio_position")) != NULL) { + pw_properties_set(oboe_props, "audio.position", str); + pw_properties_set(props, "audio_position", NULL); + } + if ((str = pw_properties_get(props, "stream_write_timeout")) != NULL) { + pw_properties_set(oboe_props, "stream.write.timeout", str); + pw_properties_set(props, "stream_write_timeout", NULL); + } + if ((str = pw_properties_get(props, "stream_properties")) != NULL) { + module_args_add_props(stream_props, str); + pw_properties_set(props, "stream_properties", NULL); + } + + d->module = module; + d->stream_props = stream_props; + d->oboe_props = oboe_props; + + return 0; +out: + pw_properties_free(stream_props); + pw_properties_free(oboe_props); + + return res; +} + +DEFINE_MODULE_INFO(module_oboe_source) = { + .name = "module-oboe-source", + .valid_args = valid_args, + .prepare = module_oboe_source_prepare, + .load = module_oboe_source_load, + .unload = module_oboe_source_unload, + .properties = &SPA_DICT_INIT_ARRAY(module_oboe_source_info), + .data_size = sizeof(struct module_oboe_source_data), +}; diff --git a/packages/qpwgraph/build.sh b/packages/qpwgraph/build.sh new file mode 100644 index 00000000000000..9f2f1428028aa2 --- /dev/null +++ b/packages/qpwgraph/build.sh @@ -0,0 +1,11 @@ +TERMUX_PKG_HOMEPAGE=https://gitlab.freedesktop.org/rncbc/qpwgraph +TERMUX_PKG_DESCRIPTION="PipeWire Graph Qt GUI Interface" +TERMUX_PKG_MAINTAINER="@termux" +TERMUX_PKG_LICENSE="GPL-2.0" +TERMUX_PKG_VERSION=0.7.2 +TERMUX_PKG_SRCURL=https://gitlab.freedesktop.org/rncbc/qpwgraph/-/archive/v$TERMUX_PKG_VERSION/qpwgraph-v$TERMUX_PKG_VERSION.tar.gz +TERMUX_PKG_SHA256=166f5f6cd87c082b65092fb9d3aa7ab16938acc78c610b36a16adc61ac8295a0 +TERMUX_PKG_DEPENDS="hicolor-icon-theme, qt6-qtbase, qt6-qtsvg, pipewire" +TERMUX_PKG_BUILD_DEPENDS="qt6-qtbase-cross-tools" +TERMUX_PKG_AUTO_UPDATE=true +TERMUX_PKG_EXTRA_CONFIGURE_ARGS="-DCONFIG_SYSTEM_TRAY=no" From 10c726bbf78cd4008755f30467f7637d9763e318 Mon Sep 17 00:00:00 2001 From: Ronald Y Date: Tue, 11 Jun 2024 01:10:37 +0800 Subject: [PATCH 2/2] jack&alsa-enabled mpv and more oboe buffer for pipewire --- packages/alsa-lib/build.sh | 5 +- packages/jack/0001-pthread_cancel.patch | 227 ++++++++++++++++++ .../0002-pthread_attr_setinheritsched.patch | 18 ++ packages/jack/build.sh | 18 ++ packages/mpv/build.sh | 3 +- packages/pipewire/0003.patch | 29 ++- packages/pipewire/module-oboe-sink.cpp | 3 +- packages/pipewire/module-oboe-source.cpp | 2 +- 8 files changed, 296 insertions(+), 9 deletions(-) create mode 100644 packages/jack/0001-pthread_cancel.patch create mode 100644 packages/jack/0002-pthread_attr_setinheritsched.patch create mode 100644 packages/jack/build.sh diff --git a/packages/alsa-lib/build.sh b/packages/alsa-lib/build.sh index 0e324bdcc6859f..805fa8dd75bc9b 100644 --- a/packages/alsa-lib/build.sh +++ b/packages/alsa-lib/build.sh @@ -5,7 +5,7 @@ TERMUX_PKG_MAINTAINER="@termux" TERMUX_PKG_VERSION="1.2.11" TERMUX_PKG_SRCURL="https://github.com/alsa-project/alsa-lib/archive/refs/tags/v$TERMUX_PKG_VERSION.tar.gz" TERMUX_PKG_SHA256=12216f0730d6dde3ded6a2a5388bc0009ad07f5c65972bd89aac9a76f8f085a4 -TERMUX_PKG_DEPENDS="libandroid-sysv-semaphore" +TERMUX_PKG_DEPENDS="libandroid-sysv-semaphore, libandroid-shmem" TERMUX_PKG_AUTO_UPDATE=true TERMUX_PKG_EXTRA_CONFIGURE_ARGS=" --disable-static @@ -13,6 +13,7 @@ TERMUX_PKG_EXTRA_CONFIGURE_ARGS=" " termux_step_pre_configure() { - LDFLAGS+=" -landroid-sysv-semaphore" + # -landroid-shmem is for packages depending on alsa-lib + LDFLAGS+=" -landroid-sysv-semaphore -landroid-shmem" autoreconf -fi } diff --git a/packages/jack/0001-pthread_cancel.patch b/packages/jack/0001-pthread_cancel.patch new file mode 100644 index 00000000000000..2c01dbb1474339 --- /dev/null +++ b/packages/jack/0001-pthread_cancel.patch @@ -0,0 +1,227 @@ +diff --git a/drivers/alsa/hammerfall.c b/drivers/alsa/hammerfall.c +index 531d73a..91eaf17 100644 +--- a/drivers/alsa/hammerfall.c ++++ b/drivers/alsa/hammerfall.c +@@ -200,7 +200,10 @@ hammerfall_release (jack_hardware_t *hw) + return; + } + ++// not patched properly because thread is never created ++#ifndef __ANDROID__ + pthread_cancel (h->monitor_thread); ++#endif + pthread_join (h->monitor_thread, &status); + + free (h); +@@ -215,7 +218,10 @@ hammerfall_monitor_controls (void *arg) + snd_ctl_elem_id_t *switch_id[3]; + snd_ctl_elem_value_t *sw[3]; + ++// not patched properly because thread is never created ++#ifndef __ANDROID__ + pthread_setcanceltype (PTHREAD_CANCEL_ASYNCHRONOUS, NULL); ++#endif + + snd_ctl_elem_id_malloc (&switch_id[0]); + snd_ctl_elem_id_malloc (&switch_id[1]); +diff --git a/drivers/freebob/freebob_driver.c b/drivers/freebob/freebob_driver.c +index d26e6dd..8f09a48 100644 +--- a/drivers/freebob/freebob_driver.c ++++ b/drivers/freebob/freebob_driver.c +@@ -795,6 +795,12 @@ freebob_driver_delete (freebob_driver_t *driver) + */ + + // the thread that will queue the midi events from the seq to the stream buffers ++#ifdef __ANDROID__ ++static void ThreadSignalHandler(int signum) ++{ ++ pthread_exit(0); ++} ++#endif + + void * freebob_driver_midi_queue_thread (void *arg) + { +@@ -807,6 +813,14 @@ void * freebob_driver_midi_queue_thread (void *arg) + int b; + int i; + ++#ifdef __ANDROID__ ++ struct sigaction actions; ++ memset(&actions, 0, sizeof(actions)); ++ sigemptyset(&actions.sa_mask); ++ actions.sa_flags = 0; ++ actions.sa_handler = ThreadSignalHandler; ++ sigaction(SIGUSR2, &actions, NULL); ++#endif + printMessage ("MIDI queue thread started"); + + while (1) { +@@ -868,6 +882,14 @@ void *freebob_driver_midi_dequeue_thread (void *arg) + + assert (m); + ++#ifdef __ANDROID__ ++ struct sigaction actions; ++ memset(&actions, 0, sizeof(actions)); ++ sigemptyset(&actions.sa_mask); ++ actions.sa_flags = 0; ++ actions.sa_handler = ThreadSignalHandler; ++ sigaction(SIGUSR2, &actions, NULL); ++#endif + while (1) { + // read incoming events + +@@ -1084,10 +1106,20 @@ freebob_driver_midi_stop (freebob_driver_midi_handle_t *m) + { + assert (m); + ++// indeed, may not necessary because pthread_testcancel() seems never called ++#ifdef __ANDROID__ ++ pthread_kill( m->queue_thread_cancel, SIGUSR2 ); ++#else + pthread_cancel (m->queue_thread); ++#endif + pthread_join (m->queue_thread, NULL); + ++// indeed, may not necessary because pthread_testcancel() seems never called ++#ifdef __ANDROID__ ++ pthread_kill( m->queue_thread_cancel, SIGUSR2 ); ++#else + pthread_cancel (m->dequeue_thread); ++#endif + pthread_join (m->dequeue_thread, NULL); + return 0; + +diff --git a/include/engine.h b/include/engine.h +index 5106713..ea11b66 100644 +--- a/include/engine.h ++++ b/include/engine.h +@@ -105,6 +105,9 @@ struct _jack_engine { + + unsigned int port_max; + pthread_t server_thread; ++#ifdef __ANDROID__ ++ atomic_flag server_thread_cancel; ++#endif + + int fds[2]; + int cleanup_fifo[2]; +diff --git a/jackd/controlapi.c b/jackd/controlapi.c +index a117e20..931b619 100644 +--- a/jackd/controlapi.c ++++ b/jackd/controlapi.c +@@ -692,7 +692,9 @@ jackctl_setup_signals ( + + setsid (); + ++#ifndef __ANDROID__ + pthread_setcanceltype (PTHREAD_CANCEL_ASYNCHRONOUS, NULL); ++#endif + + /* what's this for? + +diff --git a/jackd/engine.c b/jackd/engine.c +index 1235815..b7458e0 100644 +--- a/jackd/engine.c ++++ b/jackd/engine.c +@@ -1623,7 +1623,11 @@ jack_server_thread (void *arg) + + /* Stephane Letz: letz@grame.fr : has to be added + * otherwise pthread_cancel() does not work on MacOSX */ ++#ifdef __ANDROID__ ++ if ( !atomic_flag_test_and_set( &engine->server_thread_cancel ) ) { pthread_exit( NULL ); }; ++#else + pthread_testcancel (); ++#endif + + + /* empty cleanup FIFO if necessary */ +@@ -2038,6 +2042,9 @@ jack_engine_new (int realtime, int rtpriority, int do_mlock, int do_unlock, + + (void)jack_get_fifo_fd (engine, 0); + ++#ifdef __ANDROID__ ++ atomic_flag_test_and_set( &engine->server_thread_cancel ); ++#endif + jack_client_create_thread (NULL, &engine->server_thread, 0, FALSE, + &jack_server_thread, engine); + +@@ -2594,8 +2601,12 @@ jack_engine_delete (jack_engine_t *engine) + // MacOSX pthread_cancel still not implemented correctly in Darwin + mach_port_t machThread = pthread_mach_thread_np (engine->server_thread); + thread_terminate (machThread); ++#else ++#ifdef __ANDROID__ ++ atomic_flag_clear( &engine->server_thread_cancel ); + #else + pthread_cancel (engine->server_thread); ++#endif + pthread_join (engine->server_thread, NULL); + #endif + +diff --git a/jackd/jackd.c b/jackd/jackd.c +index b27dde2..8a808b0 100644 +--- a/jackd/jackd.c ++++ b/jackd/jackd.c +@@ -253,7 +253,9 @@ jack_main (jack_driver_desc_t * driver_desc, JSList * driver_params, JSList * sl + + setsid (); + ++#ifndef __ANDROID__ + pthread_setcanceltype (PTHREAD_CANCEL_ASYNCHRONOUS, NULL); ++#endif + + /* what's this for? + +diff --git a/libjack/client.c b/libjack/client.c +index b34766a..f97579d 100644 +--- a/libjack/client.c ++++ b/libjack/client.c +@@ -2109,7 +2109,11 @@ jack_client_core_wait (jack_client_t* client) + return -1; + } + ++#ifdef __ANDROID__ ++ if ( !atomic_flag_test_and_set( &client->thread_cancel ) ) { pthread_exit( NULL ); }; ++#else + pthread_testcancel (); ++#endif + + /* get an accurate timestamp on waking from poll for a + * process() cycle. +@@ -2379,6 +2383,9 @@ jack_start_thread (jack_client_t *client) + return -1; + } + #else ++#ifdef __ANDROID__ ++ atomic_flag_test_and_set( &client->thread_cancel ); ++#endif + if (jack_client_create_thread (client, + &client->thread, + client->engine->client_priority, +@@ -2545,7 +2552,11 @@ jack_client_close_aux (jack_client_t *client) + */ + + if (client->thread_ok) { ++#ifdef __ANDROID__ ++ atomic_flag_clear( &client->thread_cancel ); ++#else + pthread_cancel (client->thread); ++#endif + pthread_join (client->thread, &status); + } + +diff --git a/libjack/local.h b/libjack/local.h +index c90d7b0..da2e70e 100644 +--- a/libjack/local.h ++++ b/libjack/local.h +@@ -26,6 +26,9 @@ struct _jack_client { + JSList *ports_ext; + + pthread_t thread; ++#ifdef __ANDROID__ ++ atomic_flag thread_cancel; ++#endif + char fifo_prefix[PATH_MAX + 1]; + void (*on_shutdown)(void *arg); + void *on_shutdown_arg; diff --git a/packages/jack/0002-pthread_attr_setinheritsched.patch b/packages/jack/0002-pthread_attr_setinheritsched.patch new file mode 100644 index 00000000000000..978c69efeea707 --- /dev/null +++ b/packages/jack/0002-pthread_attr_setinheritsched.patch @@ -0,0 +1,18 @@ +diff --git a/libjack/thread.c b/libjack/thread.c +index a7466cc..b10cd9d 100644 +--- a/libjack/thread.c ++++ b/libjack/thread.c +@@ -175,11 +175,13 @@ jack_client_create_thread (jack_client_t* client, + #ifndef JACK_USE_MACH_THREADS + + pthread_attr_init (&attr); ++#if !defined __ANDROID__ || __ANDROID_API__ >= 28 + result = pthread_attr_setinheritsched (&attr, PTHREAD_EXPLICIT_SCHED); + if (result) { + log_result ("requesting explicit scheduling", result); + return result; + } ++#endif + result = pthread_attr_setdetachstate (&attr, PTHREAD_CREATE_JOINABLE); + if (result) { + log_result ("requesting joinable thread creation", result); diff --git a/packages/jack/build.sh b/packages/jack/build.sh new file mode 100644 index 00000000000000..1606ae9f18c03f --- /dev/null +++ b/packages/jack/build.sh @@ -0,0 +1,18 @@ +TERMUX_PKG_HOMEPAGE=https://jackaudio.org/ +TERMUX_PKG_DESCRIPTION="jack1 (non-funcitoning)" +TERMUX_PKG_MAINTAINER="@termux" +TERMUX_PKG_LICENSE="GPL-2.0, LGPL-2.0" +TERMUX_PKG_DEPENDS="libdb, libandroid-sysv-semaphore, libandroid-shmem" +TERMUX_PKG_VERSION=0.126.0 +TERMUX_PKG_GIT_BRANCH=$TERMUX_PKG_VERSION +TERMUX_PKG_SRCURL=git+https://github.com/jackaudio/jack1 +TERMUX_PKG_AUTO_UPDATE=true + +termux_step_pre_configure() { + cd ${TERMUX_PKG_SRCDIR} + git submodule update --init --recursive + ${TERMUX_PKG_SRCDIR}/autogen.sh && + cd ${TERMUX_PKG_SRCDIR} && + autoupdate + LDFLAGS+=" -landroid-shmem -landroid-sysv-semaphore" +} diff --git a/packages/mpv/build.sh b/packages/mpv/build.sh index d231498ef5c5f8..056df1a75a6db9 100644 --- a/packages/mpv/build.sh +++ b/packages/mpv/build.sh @@ -7,8 +7,7 @@ TERMUX_PKG_VERSION="0.38.0" TERMUX_PKG_SRCURL=https://github.com/mpv-player/mpv/archive/v${TERMUX_PKG_VERSION}.tar.gz TERMUX_PKG_SHA256=86d9ef40b6058732f67b46d0bbda24a074fae860b3eaae05bab3145041303066 TERMUX_PKG_AUTO_UPDATE=false -TERMUX_PKG_DEPENDS="ffmpeg, libandroid-glob, libandroid-support, libarchive, libass, libcaca, libiconv, liblua52, libsixel, libuchardet, openal-soft, pulseaudio, rubberband, zlib, libplacebo" -TERMUX_PKG_MAKE_DEPENDS="jack, alsa-lib" +TERMUX_PKG_DEPENDS="ffmpeg, libandroid-glob, libandroid-support, libarchive, libass, libcaca, libiconv, liblua52, libsixel, libuchardet, openal-soft, pulseaudio, rubberband, zlib, libplacebo, pipewire, alsa-lib, jack" TERMUX_PKG_RM_AFTER_INSTALL="share/icons share/applications" TERMUX_PKG_EXTRA_CONFIGURE_ARGS=" -Dlibmpv=true diff --git a/packages/pipewire/0003.patch b/packages/pipewire/0003.patch index 46cf0c421825c8..1958acc8c3ba7a 100644 --- a/packages/pipewire/0003.patch +++ b/packages/pipewire/0003.patch @@ -104,7 +104,7 @@ index 55a152e..ef29c33 100644 spa_log_error(state->log, "can't create log file"); return -errno; diff --git a/src/daemon/pipewire-pulse.conf.in b/src/daemon/pipewire-pulse.conf.in -index 0d54cf9..9d7759e 100644 +index 0d54cf9..74b8711 100644 --- a/src/daemon/pipewire-pulse.conf.in +++ b/src/daemon/pipewire-pulse.conf.in @@ -61,7 +61,8 @@ context.exec = [ @@ -112,8 +112,8 @@ index 0d54cf9..9d7759e 100644 # ( flags = [ nofail ] ) pulse.cmd = [ - { cmd = "load-module" args = "module-always-sink" flags = [ ] } -+ { cmd = "load-module" args = "module-oboe-sink stream_write_timeout=0" flags = [ ] } -+ { cmd = "load-module" args = "module-oboe-source stream_read_timeout=0" flags = [ ] } ++ { cmd = "load-module" args = "module-oboe-sink" flags = [ ] } ++ { cmd = "load-module" args = "module-oboe-source" flags = [ ] } { cmd = "load-module" args = "module-device-manager" flags = [ ] } { cmd = "load-module" args = "module-device-restore" flags = [ ] } { cmd = "load-module" args = "module-stream-restore" flags = [ ] } @@ -126,6 +126,29 @@ index 0d54cf9..9d7759e 100644 #"tcp:[::]:9999" # IPv6 on all addresses #"tcp:127.0.0.1:8888" # IPv4 on a single address # +diff --git a/src/daemon/pipewire.conf.in b/src/daemon/pipewire.conf.in +index 7114513..9de30a4 100644 +--- a/src/daemon/pipewire.conf.in ++++ b/src/daemon/pipewire.conf.in +@@ -225,6 +225,18 @@ context.modules = [ + flags = [ ifexists nofail ] + condition = [ { module.jackdbus-detect = true } ] + } ++ ++ # Oboe sink and source ++ { name = libpipewire-module-oboe-sink ++ args = { ++ stream.write.timeout = 5000 ++ } ++ } ++ { name = libpipewire-module-oboe-source ++ args = { ++ stream.read.timeout = 0 ++ } ++ } + ] + + context.objects = [ diff --git a/src/modules/meson.build b/src/modules/meson.build index 922a384..cd74b6f 100644 --- a/src/modules/meson.build diff --git a/packages/pipewire/module-oboe-sink.cpp b/packages/pipewire/module-oboe-sink.cpp index 91baa52f6b776c..2dca9a9c3e2515 100644 --- a/packages/pipewire/module-oboe-sink.cpp +++ b/packages/pipewire/module-oboe-sink.cpp @@ -242,6 +242,7 @@ static int open_oboe_stream(struct impl *impl) ->openStream(impl->oboe_stream)); impl->info.rate = impl->oboe_stream->getSampleRate(); + CHK(impl->oboe_stream->setBufferSizeInFrames(50 * impl->oboe_stream->getFramesPerBurst())); CHK(impl->oboe_stream->requestStart()); CHK(impl->oboe_stream->waitForStateChange(oboe::StreamState::Starting, NULL, 1000 * NANOS_PER_MILLISECOND)); @@ -524,7 +525,7 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) impl->frame_size = calc_frame_size(&impl->info); impl->stream_write_timeout = pw_properties_get_uint64(props, "stream.write.timeout", DEFAULT_STREAM_WRITE_TIMEOUT); - pw_log_debug( "stream write timeout set to %d", impl->stream_write_timeout); + pw_log_info( "stream write timeout set to %d", impl->stream_write_timeout); if (impl->frame_size == 0) { res = -EINVAL; pw_log_error( "can't parse audio format"); diff --git a/packages/pipewire/module-oboe-source.cpp b/packages/pipewire/module-oboe-source.cpp index 8defdbac319148..cbfefe4cbcb98a 100644 --- a/packages/pipewire/module-oboe-source.cpp +++ b/packages/pipewire/module-oboe-source.cpp @@ -538,7 +538,7 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) impl->frame_size = calc_frame_size(&impl->info); impl->stream_read_timeout = pw_properties_get_uint64(impl->stream_props, "stream.read.timeout", DEFAULT_STREAM_READ_TIMEOUT); - pw_log_debug( "stream read timeout set to %d", impl->stream_read_timeout); + pw_log_info( "stream read timeout set to %d", impl->stream_read_timeout); if (impl->frame_size == 0) { res = -EINVAL; pw_log_error( "can't parse audio format");