From 13c5e3c75688cdf43ff1ace26700894287f97bb5 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Wed, 17 Jun 2026 17:17:54 +0200 Subject: [PATCH] pulse-server: add a pulse.zeroramp.gap property Add a new pulse.zeroramp.gap property that will enable gap detection and fade-in/fade-out on gaps on playback streams. Make a rule to enable this on chrome, which does not do a cork/pause when a stream is paused but sends out silence. With the gap detection enabled, this allows the audioconvert to perform fades to avoid pops and clocks from sudden DC changes at the gaps. Fixes #4745 --- src/daemon/pipewire-pulse.conf.in | 10 ++++++++++ src/modules/module-protocol-pulse.c | 11 ++++++++++- src/modules/module-protocol-pulse/internal.h | 1 + src/modules/module-protocol-pulse/pulse-server.c | 5 +++++ 4 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/daemon/pipewire-pulse.conf.in b/src/daemon/pipewire-pulse.conf.in index 5c6893d7f..47c2130d9 100644 --- a/src/daemon/pipewire-pulse.conf.in +++ b/src/daemon/pipewire-pulse.conf.in @@ -142,6 +142,7 @@ pulse.properties = { #pulse.max-sample-cache = 67108864 # max total sample cache size (64MB) #pulse.default.format = F32 #pulse.default.position = [ FL FR ] + #pulse.zeroramp.gap = 0 # detect silence of N samples and fade-in/out } pulse.properties.rules = [ @@ -209,4 +210,13 @@ pulse.rules = [ # matches = [ { application.process.binary = "Discord" } ] # actions = { quirks = [ block-source-volume ] } #} + + { + matches = [ { application.process.binary = "chrome" } ] + actions = { + update-props = { + pulse.zeroramp.gap = 8 + } + } + } ] diff --git a/src/modules/module-protocol-pulse.c b/src/modules/module-protocol-pulse.c index 1a52e390e..5d928a9b9 100644 --- a/src/modules/module-protocol-pulse.c +++ b/src/modules/module-protocol-pulse.c @@ -77,7 +77,8 @@ * #pulse.default.position = [ FL FR ] * #pulse.idle.timeout = 0 * #pulse.max-streams = 64 - * #pulse.max-sample-cache = 67108864 + * #pulse.max-sample-cache = 67108864 + * #pulse.zeroramp.gap = 0 # detect silence of N samples and fade-in/out * } * * pulse.properties.rules = [ @@ -262,6 +263,14 @@ * The maximum total size in bytes of all sample cache entries. Default is * 67108864 (64MB). * + *\code{.unparsed} + * pulse.zeroramp.gap = 0 + *\endcode + * + * Enable silence detection of a playback stream and perform fade-in and fade-out on + * silence boundaries to avoid loud pops. This is a workaround for when the application + * sends silence instead of corking/uncorking to pause/resume the stream. + * * ## Command execution * * As part of the server startup sequence, a set of commands can be executed. diff --git a/src/modules/module-protocol-pulse/internal.h b/src/modules/module-protocol-pulse/internal.h index 9f9531e7e..d0d372c0e 100644 --- a/src/modules/module-protocol-pulse/internal.h +++ b/src/modules/module-protocol-pulse/internal.h @@ -38,6 +38,7 @@ struct defs { uint32_t idle_timeout; uint32_t max_streams; uint32_t max_sample_cache; + uint32_t zeroramp_gap; }; struct stats { diff --git a/src/modules/module-protocol-pulse/pulse-server.c b/src/modules/module-protocol-pulse/pulse-server.c index 807487968..6e15da265 100644 --- a/src/modules/module-protocol-pulse/pulse-server.c +++ b/src/modules/module-protocol-pulse/pulse-server.c @@ -69,6 +69,7 @@ #define DEFAULT_IDLE_TIMEOUT "0" #define DEFAULT_MAX_STREAMS "64" #define DEFAULT_MAX_SAMPLE_CACHE "67108864" +#define DEFAULT_ZERORAMP_GAP "0" #define MAX_FORMATS 32 /* The max amount of data we send in one block when capturing. In PulseAudio this @@ -1837,6 +1838,9 @@ static int do_create_playback_stream(struct client *client, uint32_t command, ui if (dont_inhibit_auto_suspend) pw_properties_set(props, PW_KEY_NODE_PASSIVE, "true"); + if (impl->defs.zeroramp_gap > 0) + pw_properties_setf(props, "zeroramp.gap", "%d", impl->defs.zeroramp_gap); + stream->stream = pw_stream_new(client->core, name, spa_steal_ptr(props)); if (stream->stream == NULL) goto error_errno; @@ -5666,6 +5670,7 @@ static void load_defaults(struct defs *def, struct pw_properties *props) parse_uint32(props, "pulse.idle.timeout", DEFAULT_IDLE_TIMEOUT, &def->idle_timeout); parse_uint32(props, "pulse.max-streams", DEFAULT_MAX_STREAMS, &def->max_streams); parse_uint32(props, "pulse.max-sample-cache", DEFAULT_MAX_SAMPLE_CACHE, &def->max_sample_cache); + parse_uint32(props, "pulse.zeroramp.gap", DEFAULT_ZERORAMP_GAP, &def->zeroramp_gap); def->sample_spec.channels = def->channel_map.channels; def->quantum_limit = 8192; }