Merge pull request #11 from The412Banner/fix/directaudio-default

Audio defaults: classic client sink, DirectAudio for games and as a client option; mic on; Share latest logs
This commit is contained in:
The412Banner authored and GitHub committed 2026-09-23 12:20:07 -04:00
commit f79dc2da43
13 files changed
+187 -22

No files matched your search

+10
View File
@@ -95,5 +95,15 @@
android:name=".session.SessionService"
android:exported="false"
android:stopWithTask="false" />
<!-- Hands a zipped session folder to the share sheet; nothing else is exposed. -->
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.logs"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/log_share_paths" />
</provider>
</application>
</manifest>
Binary file not shown.
@@ -32,6 +32,7 @@ import com.steamdeck.launcher.ui.DesktopAppsDialog
import com.steamdeck.launcher.ui.PackageRow
import com.steamdeck.launcher.session.OfflineMode
import com.steamdeck.launcher.session.ProtonExtras
import com.steamdeck.launcher.session.SessionLogShare
import com.steamdeck.launcher.session.SessionPrefs
import com.steamdeck.launcher.ui.ProtonDialog
import com.steamdeck.launcher.ui.ProtonRow
@@ -99,6 +100,7 @@ class MainActivity : ComponentActivity() {
private var prootNoSeccomp by mutableStateOf(false)
private var phantomWarning by mutableStateOf<String?>(null)
private var directAudio by mutableStateOf(false)
private var clientDirectAudio by mutableStateOf(false)
private var mic by mutableStateOf(false)
private var linuxRows by mutableStateOf<List<DriverRow>>(emptyList())
private var linuxSteam by mutableStateOf("")
@@ -240,6 +242,15 @@ class MainActivity : ComponentActivity() {
SessionPrefs.setLogsEnabled(this, !SessionPrefs.logsEnabled(this))
logsEnabled = SessionPrefs.logsEnabled(this)
},
onShareLogs = {
Thread({
val zip = runCatching { SessionLogShare.zipLatest(this) }.getOrNull()
ui.post {
if (zip == null) android.widget.Toast.makeText(this, "No session logs yet: run a session first.", android.widget.Toast.LENGTH_LONG).show()
else startActivity(SessionLogShare.shareIntent(this, zip))
}
}, "share-logs").start()
},
onOffline = {
OfflineMode.setEnabled(this, !OfflineMode.enabled(this))
offline = OfflineMode.enabled(this)
@@ -298,9 +309,18 @@ class MainActivity : ComponentActivity() {
// The session's logs land in Downloads so a failed run can be handed over as a folder
// rather than dug out of app-private storage. targetSdk 28 means the old permission still
// grants exactly that.
val wanted = ArrayList<String>()
if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
requestPermissions(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 1)
wanted.add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
}
// The microphone is on by default; ask once, with the storage prompt, so voice chat works
// without a trip to the settings. A refusal is not asked again - the toggle asks when used.
if (SessionPrefs.micEnabled(this) && !SessionPrefs.micAsked(this)
&& checkSelfPermission(Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
wanted.add(Manifest.permission.RECORD_AUDIO)
SessionPrefs.setMicAsked(this)
}
if (wanted.isNotEmpty()) requestPermissions(wanted.toTypedArray(), 1)
// A session folder left without its ending - the process was killed - gets it now.
if (!SessionState.running) Thread({ SessionArtifacts.finishAbandoned(this) }, "finish-abandoned").start()
}
@@ -394,6 +414,7 @@ class MainActivity : ComponentActivity() {
suspendPolicy = suspendPolicy,
oscMode = if (mode == SessionService.MODE_STEAM) oscMode else null,
directAudio = if (mode == SessionService.MODE_STEAM) directAudio else null,
clientDirectAudio = clientDirectAudio,
mic = if (mode == SessionService.MODE_STEAM) mic else null,
renderer = if (mode == SessionService.MODE_DESKTOP) renderer else null,
gameStorage = if (mode == SessionService.MODE_STEAM) gameStorage else null,
@@ -418,6 +439,7 @@ class MainActivity : ComponentActivity() {
onSuspendPolicy = { policy -> SessionPrefs.setSuspendPolicy(this, mode, policy); suspendPolicy = policy },
onOsc = { o -> SessionPrefs.setOscMode(this, o); oscMode = o },
onDirectAudio = { on -> SessionPrefs.setDirectAudio(this, on); directAudio = on },
onClientDirectAudio = { on -> SessionPrefs.setClientDirectAudio(this, on); clientDirectAudio = on },
onMic = { on ->
SessionPrefs.setMicEnabled(this, on)
mic = on
@@ -509,6 +531,7 @@ class MainActivity : ComponentActivity() {
suspendPolicy = SessionPrefs.suspendPolicy(this, mode)
oscMode = SessionPrefs.oscMode(this)
directAudio = SessionPrefs.directAudio(this)
clientDirectAudio = SessionPrefs.clientDirectAudio(this)
mic = SessionPrefs.micEnabled(this)
renderer = SessionPrefs.desktopRenderer(this)
gameStorage = SessionPrefs.gameStorage(this)
@@ -91,6 +91,9 @@ public class DirectAudioRelayComponent extends SessionPart {
StringBuilder command = new StringBuilder(binary.getAbsolutePath());
command.append(" --socket ").append(socketPath.getAbsolutePath());
// Diagnostics into the session's audio.log: every 1000 callbacks the device buffer,
// Android's own xrun count and the ring's fill, plus every buffer change.
command.append(" --log");
if (micFifoPath != null) {
// The helper creates the pipe if it is not there. PulseAudio is told the same path and
// reads from the other end of it.
@@ -28,7 +28,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-suspend-r4";
private static final String BUNDLE_STAMP = "2026-09-23-pa13-suspend-r5";
private final File workingDir;
/** Where the daemon's own output is kept for this session, or null for logcat only. */
@@ -63,6 +63,19 @@ public class PulseAudioComponent extends SessionPart {
this.relaySocketPath = path;
}
/** CRC-32 of the bundled pulseaudio.tzst, or "?" if it cannot be read. About 600 KB; cheap. */
private String bundleChecksum() {
java.util.zip.CRC32 crc = new java.util.zip.CRC32();
byte[] buf = new byte[64 * 1024];
try (java.io.InputStream in = app().getAssets().open("pulseaudio.tzst")) {
int n;
while ((n = in.read(buf)) > 0) crc.update(buf, 0, n);
} catch (java.io.IOException e) {
return "?";
}
return Long.toHexString(crc.getValue());
}
/** Send the daemon's output to this file as well as logcat. Set before {@link #start()}. */
public void setLogFile(File file) {
this.logFile = file;
@@ -86,17 +99,19 @@ public class PulseAudioComponent extends SessionPart {
// once ever: an installed app kept the modules it unpacked on its first run, so a bundle
// fixed in a later build never reached the device - which is how a 17.0 glibc build of
// module-pipe-source sat beside the 13.0 daemon, failed to dlopen, and the microphone
// never appeared. Bump the stamp whenever pulseaudio.tzst changes.
// never appeared. The stamp carries a checksum of the bundle itself, so any change to it -
// a rebuilt sink module included - re-unpacks, whether or not BUNDLE_STAMP was bumped.
File modulesDir = new File(workingDir, "modules");
File stamp = new File(modulesDir, ".bundle");
String have = FileUtils.readString(stamp);
String want = BUNDLE_STAMP + "-" + bundleChecksum();
if (!new File(modulesDir, "arm64/module-aaudio-sink.so").isFile()
|| !new File(workingDir, "pactl").isFile()
|| have == null || !BUNDLE_STAMP.equals(have.trim())) {
Log.i(TAG, "unpacking pulseaudio.tzst (" + BUNDLE_STAMP + "; had " + have + ")");
|| have == null || !want.equals(have.trim())) {
Log.i(TAG, "unpacking pulseaudio.tzst (" + want + "; had " + have + ")");
FileUtils.delete(modulesDir);
if (TarZst.extractAsset(app(), "pulseaudio.tzst", workingDir)) {
FileUtils.writeString(stamp, BUNDLE_STAMP);
FileUtils.writeString(stamp, want);
} else {
Log.e(TAG, "pulseaudio.tzst did not unpack");
}
@@ -128,7 +143,8 @@ public class PulseAudioComponent extends SessionPart {
config.add("load-module module-directaudio-sink sink_name=DirectAudio socket=\"" + relaySocketPath + "\" performance_mode=1 adaptive=1 volume=1.0");
config.add("set-default-sink DirectAudio");
} else {
config.add("load-module module-aaudio-sink sink_name=AAudioSink performance_mode=1 adaptive=1 volume=1.0");
// The classic sink: the adaptive AAudio module 0.1.5 shipped, unchanged.
config.add("load-module module-aaudio-classic-sink sink_name=AAudioSink performance_mode=1 adaptive=1 volume=1.0");
config.add("set-default-sink AAudioSink");
}
if (micFifoPath != null && !micFifoPath.isEmpty()) {
@@ -149,7 +165,10 @@ public class PulseAudioComponent extends SessionPart {
String command = workingDir.getAbsolutePath() + "/libpulseaudio.so"
+ " --system=false --disable-shm=true --fail=false"
+ " -n --file=default.pa --daemonize=false --use-pid-file=false --exit-idle-time=-1";
+ " -n --file=default.pa --daemonize=false --use-pid-file=false --exit-idle-time=-1"
// Info level: the classic sink reports the stream Android granted (burst, capacity,
// starting buffer) only there, and that is what a stutter report needs.
+ " --log-level=info";
// A module that refuses to load, a pipe that could not be made, the daemon exiting at
// startup: all of it used to reach logcat and nothing else, so a user's folder said nothing
// at all about sound. Four separate faults hid behind "no input device" in one night.
@@ -147,6 +147,7 @@ object DeviceReport {
k("Skip xalia", SessionPrefs.noXalia(context))
k("proot without seccomp", SessionPrefs.prootNoSeccomp(context))
k("DirectAudio for games", SessionPrefs.directAudio(context))
k("Client audio", if (SessionPrefs.clientDirectAudio(context)) "DirectAudio" else "classic")
k("Microphone", SessionPrefs.micEnabled(context))
k("On-screen controls", SessionPrefs.oscMode(context))
k("Touch mode", SessionPrefs.touchMode(context))
@@ -0,0 +1,49 @@
package com.steamdeck.launcher.session
import android.content.ClipData
import android.content.Context
import android.content.Intent
import androidx.core.content.FileProvider
import com.steamdeck.launcher.runtime.LinuxRuntime
import java.io.File
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
/**
* Zips the most recent session's log folder and hands it to Android's share sheet. Works where
* the folder could not be written to Downloads (no storage permission, or a device that refuses
* it) and landed in the app's private files instead, which a user cannot otherwise reach.
*/
object SessionLogShare {
/** The newest session folder in either place logs are written, or null if there is none. */
fun latest(context: Context): File? =
listOf(LinuxRuntime.debugLogDir(), File(context.filesDir, "logs"))
.flatMap { dir -> dir.listFiles { f -> f.isDirectory && f.name.startsWith("session-") }?.toList() ?: emptyList() }
.maxByOrNull { it.name }
/** Builds the zip (blocking). Returns null when there is no session to share. */
fun zipLatest(context: Context): File? {
val folder = latest(context) ?: return null
val out = File(context.cacheDir, "shared-logs").apply { deleteRecursively(); mkdirs() }
val zip = File(out, "SteamDeck-${folder.name}.zip")
ZipOutputStream(zip.outputStream().buffered()).use { z ->
folder.walkTopDown().filter { it.isFile }.forEach { f ->
z.putNextEntry(ZipEntry(folder.name + "/" + f.relativeTo(folder).path))
f.inputStream().use { it.copyTo(z) }
z.closeEntry()
}
}
return zip
}
fun shareIntent(context: Context, zip: File): Intent {
val uri = FileProvider.getUriForFile(context, context.packageName + ".logs", zip)
val send = Intent(Intent.ACTION_SEND)
.setType("application/zip")
.putExtra(Intent.EXTRA_STREAM, uri)
.putExtra(Intent.EXTRA_SUBJECT, zip.nameWithoutExtension)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
send.clipData = ClipData.newRawUri(zip.name, uri)
return Intent.createChooser(send, "Share session logs").addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
}
@@ -69,17 +69,35 @@ object SessionPrefs {
}
/**
* DirectAudio for the games the client launches: Wine's audio driver inside them replaced by
* ours, which talks to a helper on this side. The client itself keeps PulseAudio either way.
* The Steam client's own sound through the DirectAudio relay instead of the classic AAudio
* sink. Off by default: on an AYN Thor (Android 13, 20 ms bursts) the relay path stayed choppy
* where the classic sink - the one 0.1.5 shipped - was fine.
*/
fun directAudio(context: Context): Boolean = prefs(context).getBoolean("directAudio", false)
fun clientDirectAudio(context: Context): Boolean = prefs(context).getBoolean("clientDirectAudio", false)
fun setClientDirectAudio(context: Context, on: Boolean) {
prefs(context).edit().putBoolean("clientDirectAudio", on).apply()
}
/**
* DirectAudio for games: their Wine audio driver talks to the relay helper on this side. On
* unless the user turned it off.
*/
fun directAudio(context: Context): Boolean = prefs(context).getBoolean("directAudio", true)
fun setDirectAudio(context: Context, on: Boolean) {
prefs(context).edit().putBoolean("directAudio", on).apply()
}
/** The microphone, its own opt-in: the helper opens an input stream only when asked. */
fun micEnabled(context: Context): Boolean = prefs(context).getBoolean("mic", false)
/** The microphone for voice chat, on unless turned off; used only once RECORD_AUDIO is granted. */
fun micEnabled(context: Context): Boolean = prefs(context).getBoolean("mic", true)
/** Whether the app has already asked for the microphone once at start-up. */
fun micAsked(context: Context): Boolean = prefs(context).getBoolean("micAsked", false)
fun setMicAsked(context: Context) {
prefs(context).edit().putBoolean("micAsked", true).apply()
}
fun setMicEnabled(context: Context, on: Boolean) {
prefs(context).edit().putBoolean("mic", on).apply()
@@ -316,12 +316,14 @@ class SessionService : Service() {
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)
// The client's own sound: the classic AAudio sink unless the user chose the relay.
val clientDirectAudio = SessionState.mode == MODE_STEAM && SessionPrefs.clientDirectAudio(this)
if (clientDirectAudio) pulse.setRelaySocket(relaySocket.absolutePath)
pulse.setLogFile(audioLog)
pulse.attach(this)
guest.add("PULSE_SERVER=unix:" + pulse.socket().absolutePath)
components.add(pulse)
if (wantsDirectAudio || wantsMic) {
if (wantsDirectAudio || wantsMic || clientDirectAudio) {
// After the daemon in the list, so it can wait for the pipe the daemon makes.
val relay = DirectAudioRelayComponent(relaySocket, micFifo)
relay.setLogFile(audioLog)
@@ -335,7 +337,7 @@ class SessionService : Service() {
guest.add("BL_DIRECTAUDIO=/" + SessionFiles.DIRECTAUDIO_DIR)
guest.add("BANNER_AUDIO_DIRECT_RELAY=" + relaySocket.absolutePath)
}
Log.i(TAG, "audio: PulseAudio" + (if (wantsDirectAudio) " + DirectAudio for games" else "")
Log.i(TAG, "audio: client " + (if (clientDirectAudio) "DirectAudio" else "classic AAudio sink") + (if (wantsDirectAudio) " + DirectAudio for games" else "")
+ (if (wantsMic) " + microphone" else "") +
(if (SessionPrefs.micEnabled(this) && !wantsMic) " (microphone wanted but RECORD_AUDIO not granted)" else ""))
@@ -150,6 +150,7 @@ class FrontEndActions(
val onRoms: () -> Unit,
val onFiles: () -> Unit,
val onLogs: () -> Unit,
val onShareLogs: () -> Unit = {},
val onOffline: () -> Unit,
val onEmulatorHelp: () -> Unit,
val onCredits: () -> Unit,
@@ -363,6 +364,7 @@ private fun Rail(
) {
MenuItem("On", checked = s.logsEnabled) { if (!s.logsEnabled) a.onLogs(); menus.open = null }
MenuItem("Off", checked = !s.logsEnabled) { if (s.logsEnabled) a.onLogs(); menus.open = null }
MenuItem("Share latest logs", checked = false) { a.onShareLogs(); menus.open = null }
}
}
Box {
@@ -30,6 +30,7 @@ class ModeSettings(
/** Steam only. */
val oscMode: String?,
val directAudio: Boolean?,
val clientDirectAudio: Boolean = false,
val mic: Boolean?,
val renderer: String?,
val gameStorage: String? = null,
@@ -61,6 +62,7 @@ class ModeSettingsActions(
val onSuspendPolicy: (String) -> Unit,
val onOsc: (String) -> Unit,
val onDirectAudio: (Boolean) -> Unit,
val onClientDirectAudio: (Boolean) -> Unit = {},
val onMic: (Boolean) -> Unit,
val onRenderer: (String) -> Unit,
val onGameStorage: (path: String, label: String) -> Unit = { _, _ -> },
@@ -193,7 +195,12 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) {
)
}
if (steam && s.directAudio != null && s.mic != null) SettingsGroup("Audio") {
ToggleRow(host, "da", "DirectAudio", "Bypasses PulseAudio for lower latency, for games and the Steam client.", s.directAudio, onChange = a.onDirectAudio)
ToggleRow(host, "da", "DirectAudio for games", "Bypasses PulseAudio for lower latency in games.", s.directAudio, onChange = a.onDirectAudio)
ChoiceRow(
host, "clientAudio", "Steam client audio", "Classic is the AAudio sink from 0.1.5. DirectAudio goes through the relay. Applies next session.",
listOf("classic" to "Classic", "directaudio" to "DirectAudio"), if (s.clientDirectAudio) "directaudio" else "classic",
onPick = { id -> a.onClientDirectAudio(id == "directaudio") },
)
ToggleRow(host, "mic", "Microphone", "Uses the device microphone for voice chat.", s.mic, onChange = a.onMic)
}
if (steam && s.gameStorage != null) SettingsGroup("Game storage") {
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<cache-path name="shared_logs" path="shared-logs/" />
</paths>
+32 -5
View File
@@ -55,7 +55,8 @@ PA_MODULE_USAGE(
"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>");
"adaptive=<let the relay grow its buffer after underruns: 0 or 1> "
"buffer_ms=<device buffer the relay starts with, in ms; rounded up to whole bursts>");
#define DEFAULT_SINK_NAME "DirectAudio"
#define RECONNECT_USEC (500 * PA_USEC_PER_MSEC)
@@ -63,9 +64,18 @@ PA_MODULE_USAGE(
#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
/* Always keep at least this much queued. The relay starts its target at two bursts (8 ms), which
* a producer inside proot misses while the client is loading: two underruns in the first seconds
* on the FIT, steady stutter on a slower phone. 40 ms of slack costs 40 ms of latency at most. */
#define MIN_AHEAD_MS 40
/* The device buffer asked of the relay. Its own default is 12 ms rounded up to whole bursts: three
* 4 ms bursts on a device that grants a fast stream, but ONE 20 ms burst where Android grants a
* legacy one (an AYN Thor, Android 13), and a single burst has nothing to cover a late callback.
* 24 ms is two bursts there, Android's recommended minimum; the relay still grows it on xruns. */
#define DEFAULT_BUFFER_MS 24
static const char* const valid_modargs[] = {
"socket", "sink_name", "sink_properties", "volume", "performance_mode", "adaptive", NULL
"socket", "sink_name", "sink_properties", "volume", "performance_mode", "adaptive", "buffer_ms", NULL
};
struct userdata {
@@ -80,6 +90,7 @@ struct userdata {
char *socket_path;
int perf;
bool adaptive;
uint32_t buffer_ms;
pa_sample_spec ss;
size_t frame_size;
@@ -91,6 +102,7 @@ struct userdata {
pa_usec_t offline_at;
pa_usec_t stats_at;
uint32_t underruns_reported;
pa_usec_t settled_at;
};
static void disconnect_relay(struct userdata *u) {
@@ -132,6 +144,7 @@ static int connect_relay(struct userdata *u) {
hello.version = DA_RELAY_VERSION;
hello.flags = u->adaptive ? DA_HELLO_ADAPTIVE : 0;
hello.perf = u->perf;
hello.target_ms = (int32_t) u->buffer_ms;
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))
@@ -177,9 +190,10 @@ static int connect_relay(struct userdata *u) {
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->settled_at = u->stats_at + 10 * PA_USEC_PER_SEC;
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);
pa_log("directaudio-sink: connected to the relay: %d Hz, burst %d, device buffer %d frames (asked %u ms), ring %d frames, target %d",
ack.rate, ack.burst, ack.buf_frames, u->buffer_ms, ack.out_cap_frames, (int) u->ring->target_frames);
return 0;
fail:
@@ -239,6 +253,8 @@ static int service_ring(struct userdata *u) {
return -1;
}
target = (uint32_t) (r->target_frames > 0 ? r->target_frames : 2 * u->burst);
if (target < u->ss.rate / 1000 * MIN_AHEAD_MS)
target = u->ss.rate / 1000 * MIN_AHEAD_MS;
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);
@@ -248,6 +264,12 @@ static int service_ring(struct userdata *u) {
if (want > 0)
produce(u, want);
}
/* Once, ten seconds in: what the relay's buffer has settled at, so a session log shows it. */
if (u->settled_at && pa_rtclock_now() >= u->settled_at) {
pa_log("directaudio-sink: after 10 s: device buffer %d frames, ring target %d, relay underruns %u",
(int) r->hw_buf_frames, (int) r->target_frames, r->underruns);
u->settled_at = 0;
}
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;
@@ -333,7 +355,7 @@ int pa__init(pa_module *m) {
pa_sink_new_data data;
pa_channel_map map;
pa_cvolume volume;
uint32_t perf = 1;
uint32_t perf = 1, buffer_ms = DEFAULT_BUFFER_MS;
double linear_volume = 1.0;
bool adaptive = true;
const char *path;
@@ -352,6 +374,10 @@ int pa__init(pa_module *m) {
pa_log("performance_mode must be 0, 1 or 2");
goto fail;
}
if (pa_modargs_get_value_u32(ma, "buffer_ms", &buffer_ms) < 0 || buffer_ms > 200) {
pa_log("buffer_ms must be between 0 and 200");
goto fail;
}
if (pa_modargs_get_value_boolean(ma, "adaptive", &adaptive) < 0) {
pa_log("adaptive must be 0 or 1");
goto fail;
@@ -368,6 +394,7 @@ int pa__init(pa_module *m) {
u->socket_path = pa_xstrdup(path);
u->perf = (int) perf;
u->adaptive = adaptive;
u->buffer_ms = buffer_ms;
/* 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;