audio: the client's sound through the DirectAudio relay - module-directaudio-sink feeds the relay's render ring

A second sink module: connects to the relay's socket (lazily, the relay comes up after the
daemon; a clocked null sink until then), sends the hello, maps the render ring it gets back and
keeps it topped up to the relay's target with a futex wait per burst. Every AAudio call, the
adaptive buffer and the route handling stay in the relay, outside proot. With DirectAudio on the
daemon loads this sink instead of module-aaudio-sink; the bundle carries both.
This commit is contained in:
The412Banner committed 2026-09-23 09:12:39 -04:00
1 parent 60f393f878
commit 69aba7d0b7
7 files changed
+722 -29

No files matched your search

+3 -3
View File
@@ -24,8 +24,8 @@ jobs:
run: |
set -euo pipefail
export NDK="${ANDROID_NDK_LATEST_HOME:-${ANDROID_NDK_HOME:-$ANDROID_NDK_ROOT}}"
tools/aaudio-sink/build.sh "$PWD/pa13" "$PWD/module-aaudio-sink.so"
tools/aaudio-sink/build.sh "$PWD/pa13" "$PWD/out"
- uses: actions/upload-artifact@v4
with:
name: module-aaudio-sink
path: module-aaudio-sink.so
name: audio-sink-modules
path: out/*.so
+4 -3
View File
@@ -219,15 +219,16 @@ jobs:
run: |
set -euo pipefail
export NDK="${ANDROID_NDK_LATEST_HOME:-${ANDROID_NDK_HOME:-$ANDROID_NDK_ROOT}}"
tools/aaudio-sink/build.sh "$PWD/pa13" "$PWD/module-aaudio-sink.so"
tools/aaudio-sink/build.sh "$PWD/pa13" "$PWD/sink-out"
rm -rf pa-bundle && mkdir -p pa-bundle
tar --use-compress-program=unzstd -xf app/src/main/assets/pulseaudio.tzst -C pa-bundle
test ! -f pa-bundle/modules/arm64/module-aaudio-sink.so
install -m755 module-aaudio-sink.so pa-bundle/modules/arm64/module-aaudio-sink.so
install -m755 sink-out/module-aaudio-sink.so pa-bundle/modules/arm64/module-aaudio-sink.so
install -m755 sink-out/module-directaudio-sink.so pa-bundle/modules/arm64/module-directaudio-sink.so
(cd pa-bundle && tar --use-compress-program='zstd -19' -cf ../pulseaudio.tzst .)
mv pulseaudio.tzst app/src/main/assets/pulseaudio.tzst
tar --use-compress-program=unzstd -tf app/src/main/assets/pulseaudio.tzst | grep -c '\.so$'
rm -rf pa-bundle module-aaudio-sink.so
rm -rf pa-bundle sink-out
# Runs whether the preloads were built or restored: readelf reads any ELF whatever the host,
# so these assertions cost nothing and need no cross toolchain.
@@ -31,7 +31,7 @@ public class PulseAudioComponent extends SessionPart {
/** Where the guest reaches the daemon; the session exports PULSE_SERVER=unix:<this>. */
public static final String SOCKET_NAME = "PS0";
/** Identifies the bundled pulseaudio.tzst; a change here re-unpacks it over what a device has. */
private static final String BUNDLE_STAMP = "2026-09-23-pa13-aaudio-sink-r2";
private static final String BUNDLE_STAMP = "2026-09-23-pa13-relay-sink-r3";
private final File workingDir;
/** Where the daemon's own output is kept for this session, or null for logcat only. */
@@ -43,6 +43,8 @@ public class PulseAudioComponent extends SessionPart {
* turns that one stream into a source the client can see, named DirectAudioMic.
*/
private final String micFifoPath;
/** The DirectAudio relay's socket when the client's output should go through it, else null. */
private String relaySocketPath;
private int pid = -1;
public PulseAudioComponent(Context context) {
@@ -55,6 +57,15 @@ public class PulseAudioComponent extends SessionPart {
this.micFifoPath = micFifoPath;
}
/**
* Route the daemon's output through the DirectAudio relay at this socket instead of an AAudio
* stream of its own. The relay owns the stream outside proot, with its adaptive buffer; the
* daemon only fills a shared ring. Set before {@link #start()}; the relay may start later.
*/
public void setRelaySocket(String path) {
this.relaySocketPath = path;
}
/** Send the daemon's output to this file as well as logcat. Set before {@link #start()}. */
public void setLogFile(File file) {
this.logFile = file;
@@ -111,7 +122,11 @@ public class PulseAudioComponent extends SessionPart {
+ socket().getAbsolutePath() + "\"");
// volume=1.0 is not optional: with no volume argument module-aaudio-sink defaults
// the sink to 0% and the session plays silence.
config.add("load-module module-aaudio-sink performance_mode=1 adaptive=1 volume=1.0");
if (relaySocketPath != null && !relaySocketPath.isEmpty()) {
config.add("load-module module-directaudio-sink socket=\"" + relaySocketPath + "\" performance_mode=1 adaptive=1 volume=1.0");
} else {
config.add("load-module module-aaudio-sink performance_mode=1 adaptive=1 volume=1.0");
}
config.add("set-default-sink AAudioSink");
if (micFifoPath != null && !micFifoPath.isEmpty()) {
// The format is the helper's, fixed at s16le/48000/mono: it resamples when the device
@@ -249,6 +249,9 @@ class SessionService : Service() {
val audioLog = File(sessionDir, "audio.log")
val pulse = PulseAudioComponent(this, micFifo?.absolutePath)
// With DirectAudio on, the client's own sound goes through the relay too: the daemon
// fills the relay's ring and the relay, outside proot, drives the device.
if (wantsDirectAudio) pulse.setRelaySocket(relaySocket.absolutePath)
pulse.setLogFile(audioLog)
pulse.attach(this)
guest.add("PULSE_SERVER=unix:" + pulse.socket().absolutePath)
+29 -21
View File
@@ -1,10 +1,12 @@
#!/usr/bin/env bash
# Compiles module-aaudio-sink.so against the upstream PulseAudio 13.0 headers and the daemon's own
# libraries the app ships (app/src/main/jniLibs/arm64-v8a), with the Android NDK.
# NDK=<ndk root> tools/aaudio-sink/build.sh <pulseaudio-13.0 source dir> <output .so>
# NDK=<ndk root> tools/aaudio-sink/build.sh <pulseaudio-13.0 source dir> <output dir>
# Produces module-aaudio-sink.so and module-directaudio-sink.so in the output dir.
set -euo pipefail
PA_SRC=$1
OUT=$2
OUTDIR=$2
mkdir -p "$OUTDIR"
: "${NDK:?set NDK to the Android NDK root}"
API=26
HERE=$(cd "$(dirname "$0")" && pwd)
@@ -19,22 +21,28 @@ mkdir -p "$INC/pulse"
sed -e 's/@PA_MAJOR@/13/g' -e 's/@PA_MINOR@/0/g' -e 's/@PA_API_VERSION@/12/g' -e 's/@PA_PROTOCOL_VERSION@/33/g' \
"$PA_SRC/src/pulse/version.h.in" > "$INC/pulse/version.h"
cp "$HERE/config.h" "$HERE/ltdl.h" "$INC/"
# PulseAudio 13's own headers (pulsecore/atomic.h) predate clang's int-conversion error; that
# one is downgraded for them, the module itself compiles clean.
"$CC" -O2 -shared -fPIC -Wall -Wno-unused-parameter -Wno-error=int-conversion -Wno-visibility -DHAVE_CONFIG_H \
-I"$INC" -I"$PA_SRC/src" \
-o "$OUT" "$HERE/module-aaudio-sink.c" \
-L"$LIBS" -l:libpulsecore-13.0.so -l:libpulsecommon-13.0.so -l:libpulse.so -laaudio
"$TOOLCHAIN/llvm-strip" --strip-unneeded "$OUT"
# What the daemon looks for, and nothing linked that a device would not have.
for sym in pa__init pa__done pa__get_author pa__get_description pa__get_usage pa__get_version pa__load_once; do
"$TOOLCHAIN/llvm-nm" -D --defined-only "$OUT" | grep -q " $sym$" || { echo "ERROR: $OUT does not export $sym"; exit 1; }
done
NEEDED=$("$TOOLCHAIN/llvm-readelf" -d "$OUT" | sed -n 's/.*NEEDED.*\[\(.*\)\]/\1/p' | tr '\n' ' ')
echo "NEEDED: $NEEDED"
for lib in $NEEDED; do
case $lib in libpulsecore-13.0.so|libpulsecommon-13.0.so|libpulse.so|libaaudio.so|libc.so|libm.so|libdl.so) ;;
*) echo "ERROR: unexpected dependency $lib"; exit 1 ;;
esac
done
ls -l "$OUT"
build_one() {
local src=$1 OUT=$2 extra=$3
# PulseAudio 13's own headers (pulsecore/atomic.h) predate clang's int-conversion error; that
# one is downgraded for them, the modules themselves compile clean.
"$CC" -O2 -shared -fPIC -Wall -Wno-unused-parameter -Wno-error=int-conversion -Wno-visibility -DHAVE_CONFIG_H \
-I"$INC" -I"$PA_SRC/src" -I"$HERE" \
-o "$OUT" "$HERE/$src" \
-L"$LIBS" -l:libpulsecore-13.0.so -l:libpulsecommon-13.0.so -l:libpulse.so $extra
"$TOOLCHAIN/llvm-strip" --strip-unneeded "$OUT"
# What the daemon looks for, and nothing linked that a device would not have.
for sym in pa__init pa__done pa__get_author pa__get_description pa__get_usage pa__get_version pa__load_once; do
"$TOOLCHAIN/llvm-nm" -D --defined-only "$OUT" | grep -q " $sym$" || { echo "ERROR: $OUT does not export $sym"; exit 1; }
done
local NEEDED
NEEDED=$("$TOOLCHAIN/llvm-readelf" -d "$OUT" | sed -n 's/.*NEEDED.*\[\(.*\)\]/\1/p' | tr '\n' ' ')
echo "$(basename "$OUT") NEEDED: $NEEDED"
for lib in $NEEDED; do
case $lib in libpulsecore-13.0.so|libpulsecommon-13.0.so|libpulse.so|libaaudio.so|libc.so|libm.so|libdl.so) ;;
*) echo "ERROR: unexpected dependency $lib"; exit 1 ;;
esac
done
ls -l "$OUT"
}
build_one module-aaudio-sink.c "$OUTDIR/module-aaudio-sink.so" -laaudio
build_one module-directaudio-sink.c "$OUTDIR/module-directaudio-sink.so" ""
+209
View File
@@ -0,0 +1,209 @@
/* DirectAudio relay - the contract between the driver and the relay helper.
*
* Copyright 2026 The412Banner
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* WHY THIS EXISTS. The driver's whole premise is an in-process AAudio client:
* the guest's WASAPI buffers are mixed and handed to Android's audio server
* from inside the game, no daemon in the path. That is only possible when the
* game process is a bionic process. Under a glibc runtime (the Linux Steam
* client, where the game runs inside a proot'd Arch rootfs on Valve's own
* ARM64 Proton) libaaudio cannot be loaded at all: it is a bionic library and
* the rootfs's dynamic linker does not know how to bring it into a glibc
* process. The game side therefore has to stop at a boundary it CAN cross, and
* the only thing both worlds agree on is the kernel: a unix socket, a memfd and
* a futex word.
*
* SHAPE. One helper process (directaudio-relay, bionic, links libaaudio) runs
* on the Android side and listens on a unix socket. Each game process that
* needs audio connects once, sends its launch config (the same knobs the
* in-process build reads from BANNER_AUDIO_DIRECT_*), and gets back one or two
* shared rings (memfds passed with SCM_RIGHTS):
*
* render ring game (producer) -> helper (consumer) -> AAudio OUTPUT
* capture ring helper (producer, AAudio INPUT) -> game (consumer)
*
* The driver keeps its mixer and its per-voice WASAPI rings exactly as in the
* in-process build; what used to be the AAudio data callback is now a pump
* thread that keeps the render ring topped up, and the capture callback is a
* pump that drains the capture ring into the capture voices. Everything that
* needs an AAudioStream (open, adaptive buffer growth and decay, route-change
* reopen, the stalled-callback watchdog, the live-config mailbox) moves to the
* helper unchanged in spirit.
*
* COST. One extra hand-off: the ring. The helper's callback pulls a burst from
* the ring, so the ring must hold at least one burst at every callback; the
* driver keeps DA_RING_TARGET_BURSTS bursts queued and the helper raises that
* target when a callback finds the ring short. Everything else - the AAudio
* buffer, AudioFlinger's own latency - is what the in-process build pays too.
*
* WIRE FORMAT. Fixed-width, native-endian, same-machine only. The two sides are
* different libcs but the same CPU, so no marshalling beyond fixed-size structs.
* Bump DA_RELAY_VERSION on any layout change; the helper refuses a mismatch.
*/
#ifndef DA_RELAY_PROTO_H
#define DA_RELAY_PROTO_H
#include <stdint.h>
#include <stddef.h>
#define DA_RELAY_VERSION 1
#define DA_RING_MAGIC 0x44415247u /* 'DARG' */
#define DA_HELLO_MAGIC 0x44414831u /* 'DAH1' */
#define DA_ACK_MAGIC 0x44414131u /* 'DAA1' */
/* Socket path. The driver reads BANNER_AUDIO_DIRECT_RELAY; when unset it falls
* back to $XDG_RUNTIME_DIR/DA_RELAY_DEFAULT_NAME, which a host that already
* hands its runtime dir into the session gets for free. */
#define DA_RELAY_ENV "BANNER_AUDIO_DIRECT_RELAY"
#define DA_RELAY_DEFAULT_NAME "directaudio-relay"
/* Both rings carry float32 interleaved stereo. The render ring is always
* 48 kHz (the mixer's output rate); the capture ring runs at whatever rate the
* input stream was granted, published in da_ring.rate, and the driver resamples
* per voice as the in-process build already does. */
#define DA_RING_RATE 48000
#define DA_RING_CHANNELS 2
/* Render ring sizing. Capacity is the ceiling adaptive growth can reach; the
* initial target is what the driver keeps queued. Two bursts: the helper's
* callback consumes one burst and wakes the pump, which then has a full burst
* period to refill before the next callback. */
#define DA_RING_CAP_MS 250
#define DA_RING_TARGET_BURSTS 2
#define DA_RING_MAX_TARGET_MS 100
/* Capture ring: 200 ms is far more than a game ever leaves queued; on overrun
* the helper drops the newest block, matching the in-process "guest is not
* draining" behaviour of dropping data rather than blocking the mic thread. */
#define DA_CAPTURE_RING_MS 200
enum da_ring_state
{
DA_RING_OPENING = 0,
DA_RING_PLAYING = 1,
DA_RING_ERROR = 2,
};
struct da_ring
{
uint32_t magic;
uint32_t version;
uint32_t cap_frames; /* ring capacity in frames */
uint32_t rate; /* frames per second of the data in this ring */
uint32_t channels; /* DA_RING_CHANNELS */
uint32_t _pad0;
/* Single producer, single consumer; monotonic frame totals, wrap-safe with
* unsigned arithmetic. Producer writes widx with RELEASE after the data;
* consumer writes ridx with RELEASE after it has copied the data out. */
volatile uint32_t widx;
volatile uint32_t ridx;
/* Futex word the CONSUMER of a render ring / PRODUCER of a capture ring
* bumps after each block, so the other side can sleep on it instead of
* polling. Waiters always use a timeout, so a missed wake only costs one
* period. */
volatile uint32_t wake;
volatile uint32_t quit; /* either side: tear down */
volatile uint32_t state; /* enum da_ring_state, helper-owned */
/* Render ring only, helper-owned, read by the driver: */
volatile int32_t target_frames; /* keep at least this many queued */
volatile int32_t hw_buf_frames; /* AAudio buffer size right now (get_latency) */
volatile int32_t hw_burst; /* frames per burst of the output stream */
volatile uint32_t underruns; /* callbacks that found fewer than they needed */
volatile uint32_t cb_count; /* data callbacks so far (liveness) */
uint32_t _pad1[3];
float data[]; /* cap_frames * channels */
};
static inline size_t da_ring_bytes(uint32_t cap_frames)
{
return sizeof(struct da_ring) + (size_t)cap_frames * DA_RING_CHANNELS * sizeof(float);
}
static inline uint32_t da_ring_avail(const struct da_ring *r)
{
uint32_t w = __atomic_load_n(&r->widx, __ATOMIC_ACQUIRE);
uint32_t rd = __atomic_load_n(&r->ridx, __ATOMIC_ACQUIRE);
return w - rd;
}
static inline uint32_t da_ring_space(const struct da_ring *r)
{
return r->cap_frames - da_ring_avail(r);
}
/* ---- wire messages ------------------------------------------------------- */
#define DA_HELLO_MIC 0x01u /* also open an INPUT stream + capture ring */
#define DA_HELLO_EXCLUSIVE 0x02u /* AAUDIO_SHARING_MODE_EXCLUSIVE requested */
#define DA_HELLO_ADAPTIVE 0x04u
#define DA_HELLO_DECAY 0x08u
#define DA_HELLO_WATCHDOG 0x10u
/* First message on a fresh connection, driver -> helper. Mirrors the launch
* config the in-process build derives from BANNER_AUDIO_DIRECT_* so a preset
* means the same thing under both runtimes. */
struct da_hello
{
uint32_t magic; /* DA_HELLO_MAGIC */
uint32_t version; /* DA_RELAY_VERSION */
uint32_t flags; /* DA_HELLO_* */
int32_t perf; /* 0 NONE, 1 LOW_LATENCY, 2 POWER_SAVING (host convention) */
int32_t target_ms; /* AAudio buffer, ms; 0 = use target_frames or the default */
int32_t max_ms; /* adaptive ceiling, ms; 0 = max_frames or the default */
int32_t target_frames; /* frame forms, used only when the ms forms are 0 */
int32_t max_frames;
uint32_t stall_ms; /* watchdog: callback silent this long => rebuild */
uint32_t quiet_ms; /* decay pacing, see the driver */
uint32_t punish_ms;
uint32_t max_backoff;
int32_t pid; /* the game process, for the log */
uint32_t log; /* BANNER_AUDIO_DIRECT_LOG */
char runtime_path[256]; /* BANNER_AUDIO_DIRECT_RUNTIME mailbox, "" = none */
char name[64]; /* process name, for the log */
};
#define DA_ACK_MIC_FAILED 0x01u /* mic asked for, input stream would not open */
/* Reply, helper -> driver, followed by SCM_RIGHTS carrying the ring fds:
* fd[0] = render ring, fd[1] = capture ring (present iff in_cap_frames > 0). */
struct da_ack
{
uint32_t magic; /* DA_ACK_MAGIC */
int32_t status; /* 0 = ok, else an AAudio error code (negative) */
int32_t rate; /* output stream rate (48000) */
int32_t burst; /* output frames per burst */
int32_t buf_frames; /* output buffer size as opened */
int32_t out_cap_frames; /* render ring capacity */
int32_t in_cap_frames; /* capture ring capacity, 0 = no capture ring */
int32_t in_rate; /* rate of the capture ring's data */
uint32_t flags; /* DA_ACK_* */
uint32_t _pad;
};
/* Subsequent messages, driver -> helper, on the same socket. The helper never
* writes after the ack; everything it has to say is in the ring header. EOF on
* the socket is how the helper learns the game is gone. */
enum da_msg_type
{
DA_MSG_MIC_START = 1, /* first capture voice started: make the mic hot */
DA_MSG_MIC_STOP = 2, /* last capture voice gone: stop (keep open) */
DA_MSG_PING = 3,
};
struct da_msg
{
uint32_t type;
int32_t arg;
};
#endif /* DA_RELAY_PROTO_H */
+457
View File
@@ -0,0 +1,457 @@
/*
* module-directaudio-sink: a PulseAudio 13 sink that hands the daemon's mixed output to the
* DirectAudio relay, the bionic helper that owns the AAudio stream on the Android side.
*
* The daemon runs under proot, where every system call is traced; an AAudio stream driven from
* in here is at the mercy of that. The relay is not: it is the same helper that plays games'
* DirectAudio, with its adaptive buffer, route-change reopen and watchdog. This module is a
* producer for its render ring (da_relay_proto.h): float stereo at the ring's rate, kept topped
* up to the target the relay asks for, one futex wait per burst.
*
* The relay comes up after the daemon, so the sink loads without it and connects when the socket
* appears; until then it runs as a clocked null sink so clients are not stalled.
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <errno.h>
#include <linux/futex.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/socket.h>
#include <sys/syscall.h>
#include <sys/un.h>
#include <time.h>
#include <unistd.h>
#include <pulse/rtclock.h>
#include <pulse/timeval.h>
#include <pulse/util.h>
#include <pulse/xmalloc.h>
#include <pulsecore/core-util.h>
#include <pulsecore/log.h>
#include <pulsecore/macro.h>
#include <pulsecore/modargs.h>
#include <pulsecore/module.h>
#include <pulsecore/rtpoll.h>
#include <pulsecore/sink.h>
#include <pulsecore/thread-mq.h>
#include <pulsecore/thread.h>
#include "da_relay_proto.h"
PA_MODULE_AUTHOR("The412Banner");
PA_MODULE_DESCRIPTION("Android audio output through the DirectAudio relay");
PA_MODULE_VERSION(PACKAGE_VERSION);
PA_MODULE_LOAD_ONCE(false);
PA_MODULE_USAGE(
"socket=<the relay's unix socket> "
"sink_name=<name for the sink> "
"sink_properties=<properties for the sink> "
"volume=<initial volume, linear, 1.0 = full> "
"performance_mode=<0 none, 1 low latency, 2 power saving> "
"adaptive=<let the relay grow its buffer after underruns: 0 or 1>");
#define DEFAULT_SINK_NAME "AAudioSink"
#define RECONNECT_USEC (500 * PA_USEC_PER_MSEC)
#define OFFLINE_TICK_USEC (20 * PA_USEC_PER_MSEC)
#define STATS_EVERY_USEC (30 * PA_USEC_PER_SEC)
/* Never queue more than this ahead of the relay's target, whatever it asks for. */
#define MAX_AHEAD_MS 250
static const char* const valid_modargs[] = {
"socket", "sink_name", "sink_properties", "volume", "performance_mode", "adaptive", NULL
};
struct userdata {
pa_core *core;
pa_module *module;
pa_sink *sink;
pa_thread *thread;
pa_thread_mq thread_mq;
pa_rtpoll *rtpoll;
char *socket_path;
int perf;
bool adaptive;
pa_sample_spec ss;
size_t frame_size;
int fd;
struct da_ring *ring;
size_t ring_bytes;
int32_t burst;
pa_usec_t next_connect;
pa_usec_t offline_at;
pa_usec_t stats_at;
uint32_t underruns_reported;
};
static void disconnect_relay(struct userdata *u) {
if (u->ring) {
__atomic_store_n(&u->ring->quit, 1, __ATOMIC_RELEASE);
munmap(u->ring, u->ring_bytes);
u->ring = NULL;
}
if (u->fd >= 0) {
close(u->fd);
u->fd = -1;
}
}
/* One try at the socket; on success the render ring is mapped and u->burst known. */
static int connect_relay(struct userdata *u) {
struct sockaddr_un addr;
struct da_hello hello;
struct da_ack ack;
struct msghdr msg;
struct iovec iov;
struct cmsghdr *cm;
union { struct cmsghdr align; char buf[CMSG_SPACE(sizeof(int) * 2)]; } cmsg;
int fds[2] = { -1, -1 };
ssize_t n;
if (strlen(u->socket_path) >= sizeof(addr.sun_path))
return -1;
if ((u->fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0)) < 0)
return -1;
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
strcpy(addr.sun_path, u->socket_path);
if (connect(u->fd, (struct sockaddr*) &addr, sizeof(addr)) < 0)
goto fail;
memset(&hello, 0, sizeof(hello));
hello.magic = DA_HELLO_MAGIC;
hello.version = DA_RELAY_VERSION;
hello.flags = u->adaptive ? DA_HELLO_ADAPTIVE : 0;
hello.perf = u->perf;
hello.pid = (int32_t) getpid();
pa_snprintf(hello.name, sizeof(hello.name), "pulseaudio");
if (pa_loop_write(u->fd, &hello, sizeof(hello), NULL) != (ssize_t) sizeof(hello))
goto fail;
memset(&msg, 0, sizeof(msg));
iov.iov_base = &ack;
iov.iov_len = sizeof(ack);
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
msg.msg_control = cmsg.buf;
msg.msg_controllen = sizeof(cmsg.buf);
do {
n = recvmsg(u->fd, &msg, MSG_WAITALL);
} while (n < 0 && errno == EINTR);
if (n != (ssize_t) sizeof(ack) || ack.magic != DA_ACK_MAGIC)
goto fail;
for (cm = CMSG_FIRSTHDR(&msg); cm; cm = CMSG_NXTHDR(&msg, cm)) {
if (cm->cmsg_level == SOL_SOCKET && cm->cmsg_type == SCM_RIGHTS) {
size_t count = (cm->cmsg_len - CMSG_LEN(0)) / sizeof(int);
if (count > 2) count = 2;
memcpy(fds, CMSG_DATA(cm), count * sizeof(int));
}
}
if (fds[1] >= 0)
close(fds[1]); /* a capture ring, not for this sink */
if (ack.status != 0 || fds[0] < 0 || ack.out_cap_frames <= 0) {
pa_log("directaudio-sink: the relay refused (status %d)", ack.status);
goto fail;
}
u->ring_bytes = da_ring_bytes((uint32_t) ack.out_cap_frames);
u->ring = mmap(NULL, u->ring_bytes, PROT_READ | PROT_WRITE, MAP_SHARED, fds[0], 0);
close(fds[0]);
if (u->ring == MAP_FAILED) {
u->ring = NULL;
goto fail;
}
if (u->ring->magic != DA_RING_MAGIC || u->ring->version != DA_RELAY_VERSION || u->ring->channels != DA_RING_CHANNELS) {
pa_log("directaudio-sink: ring header mismatch");
goto fail;
}
u->burst = ack.burst > 0 ? ack.burst : 192;
if (u->ring->rate != u->ss.rate)
pa_log("directaudio-sink: the relay's ring runs at %u Hz, the sink at %u; expect resampling artefacts", u->ring->rate, u->ss.rate);
u->stats_at = pa_rtclock_now();
u->underruns_reported = u->ring->underruns;
pa_log("directaudio-sink: connected to the relay: %d Hz, burst %d, device buffer %d frames, ring %d frames, target %d",
ack.rate, ack.burst, ack.buf_frames, ack.out_cap_frames, (int) u->ring->target_frames);
return 0;
fail:
if (fds[0] >= 0) close(fds[0]);
disconnect_relay(u);
return -1;
}
static pa_usec_t frames_to_usec(const struct userdata *u, int64_t frames) {
return (pa_usec_t) (frames * (int64_t) PA_USEC_PER_SEC / (int64_t) u->ss.rate);
}
static pa_usec_t sink_latency(struct userdata *u) {
if (!u->ring)
return 0;
return frames_to_usec(u, (int64_t) da_ring_avail(u->ring) + (u->ring->hw_buf_frames > 0 ? u->ring->hw_buf_frames : 0));
}
/* Renders `frames` frames from the sink (silence when it is not open) into the ring. */
static void produce(struct userdata *u, uint32_t frames) {
struct da_ring *r = u->ring;
uint32_t w = __atomic_load_n(&r->widx, __ATOMIC_ACQUIRE);
uint32_t pos = w % r->cap_frames;
uint32_t first = PA_MIN(frames, r->cap_frames - pos);
float *dst = r->data + (size_t) pos * DA_RING_CHANNELS;
size_t bytes = (size_t) frames * u->frame_size;
if (PA_SINK_IS_OPENED(u->sink->thread_info.state)) {
pa_memchunk chunk;
const uint8_t *src;
if (u->sink->thread_info.rewind_requested)
pa_sink_process_rewind(u->sink, 0);
pa_sink_render_full(u->sink, bytes, &chunk);
src = pa_memblock_acquire_chunk(&chunk);
memcpy(dst, src, (size_t) first * u->frame_size);
if (frames > first)
memcpy(r->data, src + (size_t) first * u->frame_size, (size_t) (frames - first) * u->frame_size);
pa_memblock_release(chunk.memblock);
pa_memblock_unref(chunk.memblock);
} else {
memset(dst, 0, (size_t) first * u->frame_size);
if (frames > first)
memset(r->data, 0, (size_t) (frames - first) * u->frame_size);
}
__atomic_store_n(&r->widx, w + frames, __ATOMIC_RELEASE);
}
/* Tops the ring up to the relay's target, then sleeps until the relay drains a burst. */
static int service_ring(struct userdata *u) {
struct da_ring *r = u->ring;
uint32_t avail, target, space, wake;
struct timespec ts;
if (__atomic_load_n(&r->quit, __ATOMIC_ACQUIRE) || __atomic_load_n(&r->state, __ATOMIC_ACQUIRE) == DA_RING_ERROR) {
pa_log("directaudio-sink: the relay closed the ring; reconnecting");
return -1;
}
target = (uint32_t) (r->target_frames > 0 ? r->target_frames : 2 * u->burst);
if (target > (uint32_t) (u->ss.rate / 1000 * MAX_AHEAD_MS))
target = (uint32_t) (u->ss.rate / 1000 * MAX_AHEAD_MS);
avail = da_ring_avail(r);
space = da_ring_space(r);
if (avail < target) {
uint32_t want = PA_MIN(target - avail, space);
if (want > 0)
produce(u, want);
}
if (r->underruns != u->underruns_reported && pa_rtclock_now() - u->stats_at >= STATS_EVERY_USEC) {
pa_log("directaudio-sink: relay reports %u underrun(s), target %d frames, device buffer %d", r->underruns, (int) r->target_frames, (int) r->hw_buf_frames);
u->underruns_reported = r->underruns;
u->stats_at = pa_rtclock_now();
}
/* Sleep until the relay's callback bumps the wake word, at most one burst period. */
wake = __atomic_load_n(&r->wake, __ATOMIC_ACQUIRE);
ts.tv_sec = 0;
ts.tv_nsec = (long) (frames_to_usec(u, u->burst) * 1000);
syscall(SYS_futex, &r->wake, FUTEX_WAIT, wake, &ts, NULL, 0);
return 0;
}
static int sink_process_msg(pa_msgobject *o, int code, void *data, int64_t offset, pa_memchunk *chunk) {
struct userdata *u = PA_SINK(o)->userdata;
switch (code) {
case PA_SINK_MESSAGE_GET_LATENCY:
*((pa_usec_t*) data) = sink_latency(u);
return 0;
}
return pa_sink_process_msg(o, code, data, offset, chunk);
}
static void thread_func(void *userdata) {
struct userdata *u = userdata;
pa_log_debug("IO thread starting");
pa_thread_mq_install(&u->thread_mq);
if (u->core->realtime_scheduling)
pa_thread_make_realtime(u->core->realtime_priority);
pa_rtpoll_set_timer_relative(u->rtpoll, 0);
for (;;) {
int ret;
pa_usec_t now = pa_rtclock_now();
if (!u->ring && now >= u->next_connect) {
if (connect_relay(u) < 0) {
u->next_connect = now + RECONNECT_USEC;
u->offline_at = now;
}
}
if (u->ring) {
if (service_ring(u) < 0) {
disconnect_relay(u);
u->next_connect = pa_rtclock_now() + RECONNECT_USEC;
u->offline_at = u->next_connect;
}
pa_rtpoll_set_timer_relative(u->rtpoll, 0);
} else {
/* No relay yet: a clocked null sink, so clients keep flowing and time keeps passing. */
if (PA_SINK_IS_OPENED(u->sink->thread_info.state) && now >= u->offline_at) {
pa_memchunk chunk;
if (u->sink->thread_info.rewind_requested)
pa_sink_process_rewind(u->sink, 0);
pa_sink_render_full(u->sink, pa_usec_to_bytes(OFFLINE_TICK_USEC, &u->ss), &chunk);
pa_memblock_unref(chunk.memblock);
u->offline_at = now + OFFLINE_TICK_USEC;
}
pa_rtpoll_set_timer_absolute(u->rtpoll, PA_MIN(u->offline_at, u->next_connect));
}
if ((ret = pa_rtpoll_run(u->rtpoll)) < 0)
goto fail;
if (ret == 0)
goto finish;
}
fail:
pa_asyncmsgq_post(u->thread_mq.outq, PA_MSGOBJECT(u->core), PA_CORE_MESSAGE_UNLOAD_MODULE, u->module, 0, NULL, NULL);
pa_asyncmsgq_wait_for(u->thread_mq.inq, PA_MESSAGE_SHUTDOWN);
finish:
pa_log_debug("IO thread shutting down");
}
void pa__done(pa_module *m);
int pa__init(pa_module *m) {
struct userdata *u = NULL;
pa_modargs *ma = NULL;
pa_sink_new_data data;
pa_channel_map map;
pa_cvolume volume;
uint32_t perf = 1;
double linear_volume = 1.0;
bool adaptive = true;
const char *path;
pa_assert(m);
if (!(ma = pa_modargs_new(m->argument, valid_modargs))) {
pa_log("Failed to parse module arguments.");
goto fail;
}
if (!(path = pa_modargs_get_value(ma, "socket", NULL))) {
pa_log("socket= is required: the DirectAudio relay's unix socket");
goto fail;
}
if (pa_modargs_get_value_u32(ma, "performance_mode", &perf) < 0 || perf > 2) {
pa_log("performance_mode must be 0, 1 or 2");
goto fail;
}
if (pa_modargs_get_value_boolean(ma, "adaptive", &adaptive) < 0) {
pa_log("adaptive must be 0 or 1");
goto fail;
}
if (pa_modargs_get_value_double(ma, "volume", &linear_volume) < 0 || linear_volume < 0.0 || linear_volume > 10.0) {
pa_log("volume must be a linear factor between 0 and 10");
goto fail;
}
m->userdata = u = pa_xnew0(struct userdata, 1);
u->core = m->core;
u->module = m;
u->fd = -1;
u->socket_path = pa_xstrdup(path);
u->perf = (int) perf;
u->adaptive = adaptive;
/* The ring's format, fixed by the relay: float stereo at 48 kHz. */
u->ss.format = PA_SAMPLE_FLOAT32NE;
u->ss.rate = DA_RING_RATE;
u->ss.channels = DA_RING_CHANNELS;
u->frame_size = pa_frame_size(&u->ss);
u->burst = 192;
pa_channel_map_init_stereo(&map);
u->rtpoll = pa_rtpoll_new();
if (pa_thread_mq_init(&u->thread_mq, m->core->mainloop, u->rtpoll) < 0) {
pa_log("pa_thread_mq_init() failed.");
goto fail;
}
pa_sink_new_data_init(&data);
data.driver = __FILE__;
data.module = m;
pa_sink_new_data_set_name(&data, pa_modargs_get_value(ma, "sink_name", DEFAULT_SINK_NAME));
pa_sink_new_data_set_sample_spec(&data, &u->ss);
pa_sink_new_data_set_channel_map(&data, &map);
pa_cvolume_set(&volume, u->ss.channels, pa_sw_volume_from_linear(linear_volume));
pa_sink_new_data_set_volume(&data, &volume);
pa_proplist_sets(data.proplist, PA_PROP_DEVICE_STRING, "directaudio");
pa_proplist_sets(data.proplist, PA_PROP_DEVICE_DESCRIPTION, "Android audio output (DirectAudio)");
pa_proplist_sets(data.proplist, PA_PROP_DEVICE_CLASS, "sound");
pa_proplist_sets(data.proplist, PA_PROP_DEVICE_API, "directaudio");
if (pa_modargs_get_proplist(ma, "sink_properties", data.proplist, PA_UPDATE_REPLACE) < 0) {
pa_log("Invalid properties");
pa_sink_new_data_done(&data);
goto fail;
}
u->sink = pa_sink_new(m->core, &data, PA_SINK_HARDWARE | PA_SINK_LATENCY);
pa_sink_new_data_done(&data);
if (!u->sink) {
pa_log("Failed to create sink.");
goto fail;
}
u->sink->parent.process_msg = sink_process_msg;
u->sink->userdata = u;
pa_sink_set_asyncmsgq(u->sink, u->thread_mq.inq);
pa_sink_set_rtpoll(u->sink, u->rtpoll);
pa_sink_set_max_request(u->sink, pa_usec_to_bytes(MAX_AHEAD_MS * PA_USEC_PER_MSEC, &u->ss));
pa_sink_set_fixed_latency(u->sink, 40 * PA_USEC_PER_MSEC);
if (!(u->thread = pa_thread_new("directaudio-sink", thread_func, u))) {
pa_log("Failed to create thread.");
goto fail;
}
pa_sink_put(u->sink);
pa_modargs_free(ma);
return 0;
fail:
if (ma)
pa_modargs_free(ma);
pa__done(m);
return -1;
}
int pa__get_n_used(pa_module *m) {
struct userdata *u;
pa_assert(m);
pa_assert_se(u = m->userdata);
return pa_sink_linked_by(u->sink);
}
void pa__done(pa_module *m) {
struct userdata *u;
pa_assert(m);
if (!(u = m->userdata))
return;
if (u->sink)
pa_sink_unlink(u->sink);
if (u->thread) {
pa_asyncmsgq_send(u->thread_mq.inq, NULL, PA_MESSAGE_SHUTDOWN, NULL, 0, NULL);
pa_thread_free(u->thread);
}
pa_thread_mq_done(&u->thread_mq);
if (u->sink)
pa_sink_unref(u->sink);
disconnect_relay(u);
if (u->rtpoll)
pa_rtpoll_free(u->rtpoll);
pa_xfree(u->socket_path);
pa_xfree(u);
}