The lossless audio plane stops being an operator opt-in — PUNKTFUNK_AUDIO_HIRES defaults on #280

Merged
enricobuehler merged 1 commits from worktree-audio-hires-default-on into main 2026-08-17 11:18:59 +00:00
10 changed files with 125 additions and 68 deletions
@@ -69,8 +69,10 @@ data class Settings(
*
* Off by default and deliberately: lossless takes 2.18.5 Mbps off the top of the link,
* OUTSIDE the ABR loop that manages the video budget, against the ~256 kbps Opus it replaces —
* so it has to be asked for at both ends (`PUNKTFUNK_AUDIO_HIRES` is the host's half, also off
* by default). A REQUEST, never a fact: the host runs its gate and may answer Opus anyway, and
* so a user has to pick it. Since 2026-08-17 this setting is the ONLY opt-in: the host's half
* (`PUNKTFUNK_AUDIO_HIRES`) defaults ON and is an opt-OUT (`=0`), so this choice is enough on
* any host that has not deliberately turned the plane off.
* A REQUEST, never a fact: the host runs its gate and may answer Opus anyway, and
* the native side downgrades the rate first if THIS device will not open it. What actually
* happened is on the stats HUD, and in logcat's `audio: plane codec=… rate=…` line.
*/
@@ -789,8 +791,13 @@ val AUDIO_FORMAT_OPTIONS = listOf(
* specified* rather than *the format differs from the default*: 48 kHz/16-bit is the cheapest
* lossless rung as well as the legacy pair, so the other rule would make it the one rung nobody
* could ask for. Sending `48000`/`16` for a user who chose Standard therefore advertises the
* capability, and any host with `PUNKTFUNK_AUDIO_HIRES=1` hands that user 1.5 Mbps of lossless PCM
* instead of 256 kbps of Opus. This returned that pair until all four clients were compared.
* capability, and the host then hands that user 1.5 Mbps of lossless PCM instead of 256 kbps of
* Opus. This returned that pair until all four clients were compared.
*
* ⚠⚠ **That bug got worse on 2026-08-17, when the host's `PUNKTFUNK_AUDIO_HIRES` gate went
* default-ON.** It used to need a host whose operator had opted in — rare, so a slip here would
* have been survivable and probably unnoticed. The blast radius is now every host that has not
* deliberately opted out, i.e. all of them. The zeroes below are load-bearing.
*
* The zeroes are also what keeps a default `Hello` byte-identical to a pre-lossless one — the wire
* encodes an explicit 48 000/16 the same as absent, and the whole difference is the capability bit.
@@ -294,8 +294,9 @@ class ProfilesTest {
* both the legacy pair AND the cheapest lossless rung and the other rule would make that rung
* unrequestable. The host's gate then accepts 48 kHz/16-bit as a perfectly supported format. So
* a client that sends the legacy-looking numbers as its stand-in for "default" opts every one of
* its users in, on every host running `PUNKTFUNK_AUDIO_HIRES=1`, with no surface anywhere saying
* so — a declined session and a silently granted one look identical from the settings screen.
* its users in, on every host that has not deliberately opted out — which since 2026-08-17 is
* every host, `PUNKTFUNK_AUDIO_HIRES` having gone default-ON — with no surface anywhere saying
* so: a declined session and a silently granted one look identical from the settings screen.
*
* This client did exactly that until the four clients were compared. The rule is restated here
* rather than reached through core because Kotlin cannot call it; core's own tests pin the other
@@ -64,10 +64,12 @@ object NativeBridge {
* `CLIENT_CAP_AUDIO_HIRES` when either field is non-zero (it keys on "a format was
* specified", so that 48/16 lossless is requestable at all), and the host's gate accepts
* 48 kHz/16-bit as a supported format. Passing it as a stand-in for "unset" opts every
* session into the `0xD3` plane on any host with `PUNKTFUNK_AUDIO_HIRES=1`. Send `0`/`0`.
* session into the `0xD3` plane on every host that has not deliberately opted out — which
* since 2026-08-17 is all of them, the host gate having gone default-ON. Send `0`/`0`.
*
* A request on BOTH counts. The host runs its gate (its own `PUNKTFUNK_AUDIO_HIRES` switch
* among them, plus whether a frame of this format fits one datagram at all) and may answer
* among them — now an opt-OUT, so it declines only at `=0` — plus whether a frame of this
* format fits one datagram at all) and may answer
* Opus; and the native side first proves THIS device can open the rate — AAudio grants an
* explicit rate or fails the open, and there is no recovery once the wire is negotiated —
* walking a fallback ladder and downgrading the request if it cannot. */
+12 -7
View File
@@ -168,10 +168,14 @@ fn rate_fallback_ladder(rate_hz: u32) -> &'static [u32] {
///
/// So returning the legacy-looking `(48 000, 16)` for a user who chose **Standard (Opus)** does not
/// mean "no request" — it advertises the capability, the host's gate accepts 48 kHz/16-bit as a
/// perfectly supported format, and any host running `PUNKTFUNK_AUDIO_HIRES=1` silently gives that
/// user the lossless `0xD3` plane at 1.5 Mbps in place of 256 kbps of Opus. This returned exactly
/// that pair until it was caught by comparing all four clients; the desktop client and every
/// pre-v24 `punktfunk_connect_ex*` send `(0, 0)`, and so does this now.
/// perfectly supported format, and the host then silently gives that user the lossless `0xD3` plane
/// at 1.5 Mbps in place of 256 kbps of Opus. This returned exactly that pair until it was caught by
/// comparing all four clients; the desktop client and every pre-v24 `punktfunk_connect_ex*` send
/// `(0, 0)`, and so does this now.
///
/// ⚠⚠ The reach of that slip grew on 2026-08-17: `PUNKTFUNK_AUDIO_HIRES` went default-ON, so where
/// this used to need a host whose operator had opted in, it now lands on every host that has not
/// deliberately opted out.
///
/// `(0, 0)` is also what keeps the `Hello` byte-identical to a legacy one, because the wire encodes
/// an explicit 48 000/16 the same as absent — the difference lives entirely in the capability bit.
@@ -723,9 +727,10 @@ mod tests {
/// lossless rung, so the other rule would make that rung the one nobody could request.
///
/// The consequence is that `(48 000, 16)` is not a way of saying "default" — it is a request,
/// the host's gate accepts it as a supported format, and on any host with
/// `PUNKTFUNK_AUDIO_HIRES=1` the user who chose Standard gets the lossless `0xD3` plane instead
/// of Opus. Nothing surfaces it: the settings screen shows what was asked for, and a granted
/// the host's gate accepts it as a supported format, and the user who chose Standard gets the
/// lossless `0xD3` plane instead of Opus on every host that has not opted out of it (which,
/// since the host gate went default-ON on 2026-08-17, is all of them). Nothing surfaces it: the
/// settings screen shows what was asked for, and a granted
/// session and a declined one look identical from there. This function returned that pair until
/// all four clients were compared against each other.
///
@@ -68,8 +68,10 @@ public enum DefaultsKey {
///
/// Off by default and deliberately: lossless takes 2.18.5 Mbps off the top of the link for
/// stereo (three times that for 5.1, four for 7.1), OUTSIDE the ABR loop that manages the video
/// budget, against the ~256 kbps Opus it replaces so it must be asked for on both ends
/// (`PUNKTFUNK_AUDIO_HIRES` is the host's half, also off by default). A REQUEST: the host's
/// budget, against the ~256 kbps Opus it replaces so a user has to pick it. Since 2026-08-17
/// this row is the ONLY opt-in: the host's half (`PUNKTFUNK_AUDIO_HIRES`) defaults ON and is an
/// opt-OUT (`=0`), so picking a lossless row here is enough on any host that has not
/// deliberately turned the plane off. A REQUEST: the host's
/// five-condition gate may resolve the session back to Opus, and
/// `PunktfunkConnection.resolvedAudioRateHz`/`resolvedAudioBits`/`resolvedAudioChannels` are
/// what actually happened.
+4 -2
View File
@@ -1188,8 +1188,10 @@ pub struct Settings {
///
/// Off by default and deliberately: lossless takes 2.34.6 Mbps off the top of the link,
/// OUTSIDE the ABR loop that manages the video budget, against the ~256 kbps Opus it replaces —
/// so it has to be asked for at BOTH ends (`PUNKTFUNK_AUDIO_HIRES` is the host's half, also off
/// by default). A REQUEST, never a fact: the host runs a five-condition gate and may answer
/// so a user has to pick it. Since 2026-08-17 this setting is the ONLY opt-in: the host's half
/// (`PUNKTFUNK_AUDIO_HIRES`) defaults ON and is an opt-OUT (`=0`), so this choice is enough on
/// any host that has not deliberately turned the plane off. A REQUEST, never a fact: the host
/// runs a five-condition gate and may answer
/// Opus anyway, and this client downgrades it further if the output device will not open the
/// rate. What actually happened is the OSD's `audio lossless …` line, and the log's
/// "negotiated audio format".
+31 -22
View File
@@ -236,30 +236,37 @@ pub struct HostConfig {
/// [`punktfunk_core::audio::pcm::rate_is_supported`] and the channel count is decided by
/// whether a frame fits a datagram, not by a list — so neither is restated here.
///
/// **Default OFF, and deliberately unlike every other `Option<bool>` knob here** — the use
/// site is `unwrap_or(false)`, not `unwrap_or(true)`. `audio_redundancy` above defaults ON
/// because it costs a few hundred kbps and buys loss resilience on a plane the user already
/// agreed to; hi-res costs **1.48.5 Mbps in stereo, up to 33.9 in 7.1** and rides QUIC
/// datagrams OUTSIDE the ABR loop,
/// so it is taken off the top of the link and adaptive bitrate can neither see nor reclaim
/// it (§4.6). That is bandwidth nobody consented to, on a link the host cannot re-negotiate
/// afterwards — so it must be asked for at BOTH ends: the client sets
/// `CLIENT_CAP_AUDIO_HIRES` (its own user-facing toggle, also default off) and the operator
/// sets this.
/// **Default ON** (2026-08-17), explicit-off grammar — the same shape as `four_four_four`,
/// `chacha20` and `ten_bit` above, and for the same reason: the host merely *allows* the plane,
/// and the switch that decides any actual session is the CLIENT's, which is still default OFF.
/// `PUNKTFUNK_AUDIO_HIRES=0`/`false`/`off`/`no` disables.
///
/// `None` (unset) and an explicit off are therefore the same answer at the use site; the
/// tri-state is kept only so a future status/diagnostics reader can tell "the operator turned
/// it off" from "the operator never said". Explicit-off grammar for symmetry with its
/// neighbours.
/// It used to be default OFF, on the argument that the plane costs **1.48.5 Mbps in stereo,
/// up to 33.9 in 7.1**, rides QUIC datagrams OUTSIDE the ABR loop (§4.6) and is therefore
/// bandwidth nobody consented to. Every clause of that is still true — what was wrong was
/// asking the OPERATOR to pre-consent to it, because the operator is not who spends it and, on
/// a host with no settings UI, is a person who has to be told an environment variable exists
/// before a user's own explicit menu choice can work at all. The field report that moved this:
/// a user picked "Lossless 96 kHz / 24-bit" in the macOS client, got Opus, and nothing in any
/// UI said why — the reason was one `INFO` line in the host's journal.
///
/// What actually protects the link is the rest of the §8.4 gate, and it is mechanical rather
/// than consent-based: the client must have asked, the capture path must HONESTLY deliver the
/// rate (not merely open at it), the cost must fit `HIRES_MAX_VIDEO_SHARE_PCT` (25 %) of the
/// session's video bitrate, and a frame must fit a datagram. A 5 Mbps session still cannot buy
/// 96/24 no matter what this says. So the operator gate was never the thing keeping a modest
/// link safe — it was only keeping the feature unreachable.
///
/// ⚠ **The desktop CLIENTS read a variable of this same name with a RICHER grammar** — see
/// `pf_client_core::session`, which takes `1`/`on`, a bare rate such as `96000`, or an explicit
/// `<rate>/<bits>`. A box that is both host and client therefore configures both halves from
/// one environment line, and they compose only because [`env_on`] reads everything that is not
/// `0`/`false`/`off`/`no` as *on*: a client-shaped `96000/24` happens to say *allow* here too.
/// That is an accident that works, not a shared grammar — `1` is the only spelling that means
/// the same thing at both ends, which is why it is the one the docs give.
pub audio_hires: Option<bool>,
/// one environment line, and they still compose after the flip: [`env_on`] reads everything
/// that is not `0`/`false`/`off`/`no` as *on*, so a client-shaped `96000/24` says *allow* here
/// too — and the one spelling that has to mean the same thing at both ends is now `0`, which
/// does: it forces Opus on the host and forces Opus at the client. The interesting direction
/// reversed with the default. It used to be "did anyone remember to turn this on"; it is now
/// "did anyone turn it off".
pub audio_hires: bool,
/// `PUNKTFUNK_PERF` — per-stage timing instrumentation.
pub perf: bool,
/// `PUNKTFUNK_VIDEO_SOURCE` — GameStream video source select. `virtual` (the default — a
@@ -465,9 +472,11 @@ impl HostConfig {
audio_output_mode: AudioOutputMode::from_env(),
audio_quality: val("PUNKTFUNK_AUDIO_QUALITY").map(|s| s.trim().to_lowercase()),
audio_redundancy: env_on("PUNKTFUNK_AUDIO_REDUNDANCY"),
// Tri-state like its neighbour, but read as `unwrap_or(FALSE)` at the use site —
// see the field doc for why this one knob inverts the house default.
audio_hires: env_on("PUNKTFUNK_AUDIO_HIRES"),
// Default ON, explicit-off grammar (the client's CLIENT_CAP_AUDIO_HIRES bit — and the
// audio-format row its user picked — is the real per-session switch; the §8.4 gate's
// capture, bandwidth and datagram conditions are what keep a modest link safe, not
// this. See the field doc for why it stopped being opt-in).
audio_hires: env_on("PUNKTFUNK_AUDIO_HIRES").unwrap_or(true),
perf: flag("PUNKTFUNK_PERF"),
// Default ON while the interval-stutter field program runs (see the field doc).
stall_probes: env_on("PUNKTFUNK_STALL_PROBES").unwrap_or(true),
+34 -13
View File
@@ -171,8 +171,13 @@ const HIRES_MAX_VIDEO_SHARE_PCT: u32 = 25;
/// 1. `client_asked` — the client set `CLIENT_CAP_AUDIO_HIRES`. Capable **and** the user turned
/// it on, the `VIDEO_CAP_444` precedent: a client that cannot open a 96 kHz output, or whose
/// user never asked, must not set the bit.
/// 2. `operator_allows` — `PUNKTFUNK_AUDIO_HIRES`, default OFF. This spends bandwidth the host's
/// owner did not previously agree to, so it is asked for at both ends.
/// 2. `operator_allows` — `PUNKTFUNK_AUDIO_HIRES`, **default ON since 2026-08-17**; the operator
/// opts OUT with `=0`. It was default off, on the argument that this spends bandwidth the
/// host's owner never agreed to — but the operator is not who spends it, the client's user is,
/// and condition 1 is already that user's explicit choice. What a default-off operator gate
/// actually produced was a user picking "Lossless 96 kHz / 24-bit" in the client, silently
/// getting Opus, and the reason living in one `INFO` line of the host's journal. Conditions
/// 3 and 4 are what keep a link safe, and they are mechanical rather than consent-based.
/// 3. `capture_rate` — the capture path can GENUINELY deliver the requested rate (§8.2 / §8.3).
/// Not "did the open succeed": both backends accept a rate their endpoint does not run at and
/// resample to it without an error, so the question has to be put to the DEVICE before the
@@ -226,10 +231,16 @@ pub(super) fn resolve_audio_plane(
// ⚠ The range is the OPERATOR's decision criterion, so it has to keep up with what
// the plane can now negotiate: 1.4 Mbps at 44.1/16 stereo up to 8.5 at 176.4/24, and
// up to 33.9 for 176.4/24 7.1. It read "1.54.6" while the plane was 48/96 stereo.
"hi-res audio requested by the client but PUNKTFUNK_AUDIO_HIRES is not enabled on \
this host — the session uses Opus 48 kHz (the lossless plane costs 1.48.5 Mbps in \
stereo, and up to 33.9 in 7.1, off the top of the link — so it is opt-in on both \
ends)"
//
// ⚠ This is now the DELIBERATE-OPT-OUT message, not the forgot-to-opt-in one: the gate
// defaults ON, so reaching this line means someone set the variable to `0` on this
// host. Naming the variable AND the value it must have is the difference between an
// operator finding their own `host.env` line and re-reading the docs for a switch they
// never set. The old wording sent people looking for something to enable.
"hi-res audio requested by the client but it is disabled on this host by \
PUNKTFUNK_AUDIO_HIRES=0 — the session uses Opus 48 kHz (remove that line, or set it \
to 1, to allow the lossless plane; it costs 1.48.5 Mbps in stereo and up to 33.9 in \
7.1, off the top of the link and outside the ABR loop)"
);
return AudioPlane::opus();
}
@@ -851,7 +862,7 @@ pub(super) async fn negotiate(
staying on Opus; the capability and the format must be set together"
);
}
let hires_allowed = pf_host_config::config().audio_hires.unwrap_or(false);
let hires_allowed = pf_host_config::config().audio_hires;
// §8.4 condition 4 — what the capture path can HONESTLY deliver, asked of the device rather
// than inferred from a successful open (§4.3/§4.4: both backends resample a rate they cannot
// run at, without an error). Blocking on Windows (an endpoint enumeration plus an
@@ -860,7 +871,10 @@ pub(super) async fn negotiate(
//
// Short-circuited behind the two cheap policy conditions, which is the same discipline those
// probes use: an ordinary session — every session with every shipping client today — must not
// pay COM work for a feature nobody asked for. The value is not merely unused in that case
// pay COM work for a feature nobody asked for. ⚠ Since the operator gate went default-ON that
// guarantee rests on `hires_asked` ALONE, so keep it first: a client that does not set
// CLIENT_CAP_AUDIO_HIRES is now the only thing standing between an ordinary Windows session
// and an endpoint enumeration it has no use for. The value is not merely unused in that case
// but unreachable, since the gate returns on condition 1 or 2 before it looks at this one;
// `Unknown` is nonetheless the correct thing to pass, because "we did not ask" and "we asked
// and could not tell" both mean the same thing to the gate: decline.
@@ -1225,8 +1239,10 @@ mod tests {
assert_eq!(p, AudioPlane::opus());
}
/// §8.4 condition 2 — the operator's `PUNKTFUNK_AUDIO_HIRES` gate, default OFF. A client
/// asking is not enough on its own: this costs bandwidth the host's owner never agreed to.
/// §8.4 condition 2 — the operator's `PUNKTFUNK_AUDIO_HIRES` gate. Default ON since
/// 2026-08-17, so this is now the OPT-OUT path (`=0`) rather than the un-opted-in one: an
/// operator who has deliberately turned the plane off outranks a client that asks for it, and
/// still gets today's wire.
#[test]
fn the_operator_gate_alone_can_decline() {
let p = resolve_audio_plane(
@@ -1242,11 +1258,16 @@ mod tests {
assert_eq!(p, AudioPlane::opus());
}
/// …and the default really is off, so an operator who has set nothing gets today's wire.
/// …and the default really is ON, so an operator who has set nothing no longer has to discover
/// an environment variable before a client's own explicit audio-format choice can be honoured.
/// (`config()` reads the process environment once; no test in this crate sets the knob.)
///
/// ⚠ This asserts the DEFAULT, not that any session goes lossless: condition 1 still gates
/// every one of them on the client's `CLIENT_CAP_AUDIO_HIRES`, which ships off. The two
/// neighbouring tests above are what prove an ordinary session is untouched by this flip.
#[test]
fn the_operator_default_is_off() {
assert!(!pf_host_config::config().audio_hires.unwrap_or(false));
fn the_operator_default_is_on() {
assert!(pf_host_config::config().audio_hires);
}
/// Surround, decided by the FRAME LADDER rather than by a stereo-only rule — the whole point
+2 -2
View File
@@ -157,7 +157,7 @@ See your desktop page ([KDE](/docs/kde), [GNOME](/docs/gnome)) for when to set t
|---|---|---|
| `PUNKTFUNK_AUDIO_QUALITY` | `low` · `standard` · `high` *(default `high`)* | Desktop-audio encode quality. `high` (stereo 256 kbps Opus, effectively transparent) costs about 1 % of a normal video bitrate, so there's rarely a reason to go lower. `standard` is exactly the pre-0.25 encoder (stereo 128 kbps) — handy for an A/B comparison; `low` is for genuinely constrained links (noticeably lossy on music, still fine for game audio and voice). A typo warns in the log and keeps `high` rather than silently downgrading. Host-side only — clients play whatever arrives, no client setting involved. |
| `PUNKTFUNK_AUDIO_REDUNDANCY` | `1` · `0` *(default: automatic)* | Send audio packets redundantly so a lossy link doesn't crackle. Leave it unset: the host turns redundancy on by itself, only toward clients that support it and only while the link is actually losing packets. `1` forces it on for the whole session, `0` never sends it. |
| `PUNKTFUNK_AUDIO_HIRES` | `1` · `0` *(default off)* | Allow the **lossless** audio plane — uncompressed 48/96 kHz, 16/24-bit stereo PCM instead of Opus. Off unless you set it, *and* off unless the client asks for it too: both ends have to opt in, because it costs **1.54.6 Mbps** where Opus costs 256 kbps, and audio rides QUIC datagrams outside the adaptive-bitrate loop — so that is taken off the top of the link, and ABR can neither see it nor claw it back when the connection tightens. Be clear about what it buys: on game content it is very unlikely to be *audible* (256 kbps Opus is already effectively transparent, and nothing above 24 kHz is hearable at all), so the real win is **bit-exactness** — no lossy stage anywhere, and no resample for a host whose interface genuinely runs at 96 kHz. If any condition fails — the client didn't ask, the session isn't stereo, the capture device can't genuinely deliver the rate, or the link can't spare the bandwidth — the session quietly stays on Opus and the host log names which one lost. ⚠️ **The desktop clients read a variable of this same name with a richer grammar** (see [Client-side](#client-side-native-clients) below), so on a box that is both host and client, one line configures both ends. `1` is the one spelling that means *on* to each: this host gate accepts anything that isn't `0`/`false`/`off`/`no`, so a client-style `96000/24` happens to read as *allow* here too, but only `1` says the same thing on both sides. |
| `PUNKTFUNK_AUDIO_HIRES` | `0` · `1` *(default: allowed)* | Whether this host will serve the **lossless** audio plane — uncompressed PCM (44.1 / 48 / 88.2 / 96 / 176.4 kHz, 16 or 24-bit, stereo through 7.1) instead of Opus. **You do not need to set this** — since 0.30 the host allows it and the *client's* audio-format setting is the opt-in, which is the switch belonging to the person whose bandwidth it spends. `0` refuses the plane on this host no matter what any client asks for. It was an operator opt-in until 2026-08-17, and what that produced was users picking "Lossless 96 kHz / 24-bit" in a client, silently getting Opus, and the reason existing only as one `INFO` line in the host's journal. What still protects the link is mechanical rather than a pre-agreement: the plane costs **1.48.5 Mbps** in stereo (up to 33.9 for 176.4 kHz/24-bit 7.1) against Opus's 256 kbps, it rides QUIC datagrams outside the adaptive-bitrate loop — off the top of the link, where ABR can neither see it nor claw it back — and so a session gets it only if the cost fits **a quarter of that session's video bitrate**. A 5 Mbps session can afford no rung of it at all. Be clear about what it buys: on game content it is very unlikely to be *audible* (256 kbps Opus is already effectively transparent, and nothing above 24 kHz is hearable at all), so the real win is **bit-exactness** — no lossy stage anywhere, and no resample for a host whose interface genuinely runs at 96 kHz. If any condition fails — the client didn't ask, this variable is `0`, the capture device can't genuinely deliver the rate, the link can't spare the bandwidth, or one frame of that format won't fit a datagram at that channel count — the session quietly stays on Opus and the host log names which one lost. ⚠️ **The desktop clients read a variable of this same name with a richer grammar** (see [Client-side](#client-side-native-clients) below), so on a box that is both host and client, one line configures both ends. `0` is now the interesting shared spelling, and it means *off* to each of them; this host gate reads anything that isn't `0`/`false`/`off`/`no` as *allow*, so a client-style `96000/24` leaves this half permissive. |
| `PUNKTFUNK_AUDIO_GAIN` | float (default `1.0`) | Gain applied to captured desktop audio — bump it for a quiet source. Applies to **both** the native `punktfunk/1` and Moonlight/GameStream paths. Peaks are rounded off by a soft limiter rather than clipped, so a boost distorts gracefully instead of abruptly; values above `8.0` (+18 dB) are capped, and a non-positive value is ignored. Note this buys **headroom, not loudness** — it cannot make a desktop mix as loud as already-limited streaming-app audio, and pushing it hard to try will audibly squash the signal. On Windows this is the only host-side control that works at all: loopback capture is tapped upstream of the endpoint's master volume, so the speaker slider does not affect what a client receives. |
| `PUNKTFUNK_MIC_DEVICE` | name substring | **(Windows)** Target mic-uplink device by friendly-name substring (first match wins). |
| `PUNKTFUNK_MIC_LEGACY_BUFFER` | `1` | Restore the fixed pre-adaptive mic buffering (a ~48 ms prime and ~120 ms cap on Windows; a buffer scaled to the recording app's audio quantum on Linux) instead of the adaptive per-client jitter target. One-release escape hatch: if the microphone coming out of the host only sounds right *with* this set, that's a bug — please report it. |
@@ -274,7 +274,7 @@ table, where client and host read the *same* variable name for their own half of
| `PUNKTFUNK_PAD_SPEAKER_PATH` · `PUNKTFUNK_PAD_SPEAKER_VOLUME` | byte, hex or decimal *(default `0x20` / `0x7F`)* | Which output a DualSense sends [controller audio](/docs/controller-audio) to, and how loud. A controller's channel 1 is shared between its headphone jack and its built-in speaker, and it powers up pointing at the jack — so with no headphones plugged in the speaker stays silent however correctly the audio is routed. Punktfunk points it at the speaker when controller-speaker is on. Change these only if your pad's speaker stays quiet; a game that sets its own audio levels still overrides them. |
| `PUNKTFUNK_PAD_AUDIO_PROFILE` | `0` | **(Linux)** Stop the client from switching a wired DualSense's sound card to **Pro Audio** while it streams [controller audio](/docs/controller-audio) to it. The switch exists because a controller's voice coils are channels 3 and 4 of its sound card, and a controller almost never presents four channels on its own — on any other profile the haptics are folded into the speaker pair and felt as nothing. Punktfunk restores the card's profile when the session ends and never saves it. Set this if you'd rather select the card's profile yourself. |
| `PUNKTFUNK_OSD_SCALE` | multiplier, e.g. `1.5` *(default `1`)* | Size of the in-stream overlay — the stats OSD, the capture hint and the start banner. They already follow your display's scaling setting (200 % display → twice the pixels), so set this only to nudge that: bigger for a TV across the room, smaller if your compositor reports an aggressive scale. Clamped to 0.5×–4×, and a line that would run off the screen is shrunk to fit. |
| `PUNKTFUNK_AUDIO_HIRES` | `1`/`on`/`true`/`yes` · `48000` · `96000` · `<rate>/<bits>` · `0`/`off`/`false`/`no` *(unset: the client's stored audio-format choice decides)* | ⚠️ **The same name as the host's policy gate in Audio / microphone above, and a different grammar** — so one line on a box that is both host and client sets both halves. This is the **request** half, and it overrides the client's stored audio-format choice for the run. `1` asks for 96 kHz / 24-bit, the rung the plane earns its bandwidth at. A bare rate — `48000` or `96000` — asks for that rate at 24-bit. `<rate>/<bits>` names both, which is the only way to reach `48000/16`: the cheapest lossless rung (~1.5 Mbps), and one no menu offers, because at 16-bit there is nothing left to *hear* over the 256 kbps Opus it replaces — only bit-exactness. `0` forces Opus even when the stored choice asks for lossless. Anything else is a typo: the client warns and **ignores** it, so the stored choice still decides rather than being silently switched off. And asking is not getting — the host's own gate, stereo, a capture path that genuinely delivers the rate and the link budget all still have to agree, and the client plays whatever the host answers. Linux and Windows clients. |
| `PUNKTFUNK_AUDIO_HIRES` | `1`/`on`/`true`/`yes` · `48000` · `96000` · `<rate>/<bits>` · `0`/`off`/`false`/`no` *(unset: the client's stored audio-format choice decides)* | ⚠️ **The same name as the host's policy gate in Audio / microphone above, and a different grammar** — so one line on a box that is both host and client sets both halves. This is the **request** half, and it overrides the client's stored audio-format choice for the run. `1` asks for 96 kHz / 24-bit, the rung the plane earns its bandwidth at. A bare rate — `48000` or `96000` — asks for that rate at 24-bit. `<rate>/<bits>` names both, which is the only way to reach `48000/16`: the cheapest lossless rung (~1.5 Mbps), and one no menu offers, because at 16-bit there is nothing left to *hear* over the 256 kbps Opus it replaces — only bit-exactness. `0` forces Opus even when the stored choice asks for lossless. Anything else is a typo: the client warns and **ignores** it, so the stored choice still decides rather than being silently switched off. And asking is not getting — a capture path that genuinely delivers the rate, the link budget, a frame that fits a datagram at that channel count, and the host not having set its own half to `0` all still have to agree, and the client plays whatever the host answers. (The host's half no longer has to be turned *on*, as it did before 2026-08-17 — so this request is usually the only one that matters.) Linux and Windows clients. |
| `PUNKTFUNK_NO_AEC` | `1` | Turn the microphone's echo cancellation off for this run, whatever **Echo cancellation** says in [client settings](/docs/client-settings#audio). One-way: it can only switch the processing off, never back on, and the setting is the normal way to control it. Linux and Windows clients. |
| `PUNKTFUNK_PRESENT_MODE` | `mailbox` *(default)* · `fifo` · `immediate` · `fifo_relaxed` | How decoded frames meet the display (the Vulkan present mode). The default prefers MAILBOX — tear-free without queueing behind the vertical refresh — and falls back to FIFO (classic vsync) where the driver doesn't offer it. **AMD's Windows driver offers no MAILBOX**, so those clients run FIFO, which adds a standing frame-pacing wait (up to one refresh interval). `immediate` removes that wait but can tear; `fifo_relaxed` only tears when a frame is late. If your latency floor matters more than tearing, try `immediate` and judge by eye. |
| `PUNKTFUNK_PRESENTER` | `arrival` | Turn the frame-pacing engine off for this run: frames present the instant they decode, exactly as they did before the **Prioritize** setting existed. A diagnostic — if a pacing change is suspected of causing judder or added delay, this switches it off without reinstalling anything. Linux and Windows clients. |
+20 -12
View File
@@ -557,17 +557,21 @@ INFO punktfunk/1 audio streaming … tier=high kbps=512 redundancy=true
`standard` reproduces the pre-0.25 encoder exactly if you want to A/B it.
If what you want is **no lossy stage at all**, there is a third knob — but read what it costs
first:
If what you want is **no lossy stage at all**, ask for it in the client: its audio-format setting
has a **Lossless** row per rate, and picking one is the whole opt-in. There is nothing to set on the
host — the host allows the lossless plane by default. If you want to forbid it on this host
regardless of what clients ask for, that is the same variable, inverted:
```ini
PUNKTFUNK_AUDIO_HIRES=1 # allow the lossless PCM audio plane (default off)
PUNKTFUNK_AUDIO_HIRES=0 # refuse the lossless PCM audio plane (default: allowed)
```
That replaces Opus with uncompressed 48/96 kHz, 16/24-bit stereo PCM. It has to be turned on at
**both** ends — the client has its own switch, also off by default — because it costs 1.54.6 Mbps
against Opus's 256 kbps, and like every other audio setting here that comes off the top of the
link, where adaptive bitrate can neither see it nor reclaim it.
The plane replaces Opus with uncompressed PCM — 44.1 through 176.4 kHz, 16 or 24-bit, stereo
through 7.1 — and it costs 1.48.5 Mbps in stereo (up to 33.9 for 176.4 kHz/24-bit 7.1) against
Opus's 256 kbps. Like every other audio setting here, that comes off the top of the link, where
adaptive bitrate can neither see it nor reclaim it. Which is why the client's setting ships off and
the host's affordability check is unconditional: a session only goes lossless when it can pay for
it out of a quarter of its video bitrate.
It is also unlikely to fix the problem *this* section is about: a lossless copy of a 24 kHz mono
mix is still a 24 kHz mono mix, so fix the endpoint first. What it buys is bit-exactness rather
@@ -576,14 +580,18 @@ Windows the host reads the endpoint's own engine rate (the `engine_hz` line abov
pad, so 96 kHz means setting that device to 96 kHz in Windows' own sound properties. A Linux host
normally owns the sink applications play into and states its rate to the audio graph itself, so
96 kHz there needs no device configuration at all. Whenever any condition fails — the client didn't
ask, the session isn't stereo, the capture path can't genuinely deliver the rate, or the link can't
spare it — the session quietly stays on Opus and the log says which one lost.
ask, this host has `PUNKTFUNK_AUDIO_HIRES=0`, the capture path can't genuinely deliver the rate, the
link can't spare it, or one frame of that format won't fit a datagram at that channel count — the
session quietly stays on Opus and the log says which one lost. That log line is worth knowing about
before you go looking in the UI: a declined session and a granted one look the same from the
settings screen, and the host's journal is where the reason lives.
One trap if the box you are editing is *also* a client: the Linux and Windows clients read a
`PUNKTFUNK_AUDIO_HIRES` of their own, with a richer grammar — a bare rate such as `96000`, or an
explicit `96000/24` — so one line in a shared environment sets both halves at once. **`1` is the
value that means *on* to each of them**, which is why the line above is written that way. The
client's spellings are in
explicit `96000/24` — so one line in a shared environment sets both halves at once. **`0` is the
value that means *off* to each of them**, which is why the opt-out line above is written that way;
anything that isn't `0`/`false`/`off`/`no` reads as *allow* on the host side, so a client-shaped
`96000/24` also leaves the host's half permissive. The client's spellings are in
[Configuration → Client-side](/docs/configuration#client-side-native-clients).
## Audio lags behind the picture