src/mainwindow.cc | 34 +++++++++++++++++----- src/minimalstreamwidget.cc | 67 ++++++++++++++++++++++++++++++++++++--------- src/minimalstreamwidget.h | 10 +++++- 3 files changed, 88 insertions(+), 23 deletions(-)
New commits: commit ce48aec45ffc551c6456f74a5134f23153f322f5 Author: Arun Raghavan <a...@asymptotic.io> Date: Fri Jun 6 11:50:12 2025 +0530 Decay volume meters on monitor stream suspend If the stream is suspended mid peak, the volume meter just hangs at the last level. This can happen with a idle suspend timeout of 0 on PulseAudio, or just by default on PipeWire. When that happens, we attach to the frame clock and decay to zero in a second. Fixes: https://gitlab.freedesktop.org/pulseaudio/pavucontrol/-/issues/174 diff --git a/src/mainwindow.cc b/src/mainwindow.cc index fa4b48e..c6ca964 100644 --- a/src/mainwindow.cc +++ b/src/mainwindow.cc @@ -626,7 +626,7 @@ static void suspended_callback(pa_stream *s, void *userdata) { MainWindow *w = static_cast<MainWindow*>(userdata); if (pa_stream_is_suspended(s)) - w->updateVolumeMeter(pa_stream_get_device_index(s), PA_INVALID_INDEX, -1); + w->updateVolumeMeter(pa_stream_get_device_index(s), pa_stream_get_monitor_stream(s), -1); } static void read_callback(pa_stream *s, size_t length, void *userdata) { @@ -1091,13 +1091,15 @@ void MainWindow::updateDeviceInfo(const pa_ext_device_restore_info &info) { void MainWindow::updateVolumeMeter(uint32_t source_index, uint32_t sink_input_idx, double v) { + MinimalStreamWidget *sw = NULL; if (sink_input_idx != PA_INVALID_INDEX) { SinkInputWidget *w; if (sinkInputWidgets.count(sink_input_idx)) { w = sinkInputWidgets[sink_input_idx]; - w->updatePeak(v); + sw = static_cast<MinimalStreamWidget*>(w); + goto done; } } else { @@ -1105,22 +1107,38 @@ void MainWindow::updateVolumeMeter(uint32_t source_index, uint32_t sink_input_id for (std::map<uint32_t, SinkWidget*>::iterator i = sinkWidgets.begin(); i != sinkWidgets.end(); ++i) { SinkWidget* w = i->second; - if (w->monitor_index == source_index) - w->updatePeak(v); + if (w->monitor_index == source_index) { + sw = static_cast<MinimalStreamWidget*>(w); + goto done; + } } for (std::map<uint32_t, SourceWidget*>::iterator i = sourceWidgets.begin(); i != sourceWidgets.end(); ++i) { SourceWidget* w = i->second; - if (w->index == source_index) - w->updatePeak(v); + if (w->index == source_index) { + sw = static_cast<MinimalStreamWidget*>(w); + goto done; + } } for (std::map<uint32_t, SourceOutputWidget*>::iterator i = sourceOutputWidgets.begin(); i != sourceOutputWidgets.end(); ++i) { SourceOutputWidget* w = i->second; - if (w->sourceIndex() == source_index) - w->updatePeak(v); + if (w->sourceIndex() == source_index) { + sw = static_cast<MinimalStreamWidget*>(w); + goto done; + } + } + } + +done: + if (sw) { + if (v == -1) { + sw->decayToZero(); + } else { + sw->stopDecay(); + sw->updatePeak(v); } } } diff --git a/src/minimalstreamwidget.cc b/src/minimalstreamwidget.cc index 92e5f08..0a85538 100644 --- a/src/minimalstreamwidget.cc +++ b/src/minimalstreamwidget.cc @@ -36,7 +36,9 @@ MinimalStreamWidget::MinimalStreamWidget(BaseObjectType* cobject) : peak(NULL), updating(false), volumeMeterEnabled(false), - volumeMeterVisible(true) { + volumeMeterVisible(true), + decayTickId(0), + decayLastFrameTime(-1) { } MinimalStreamWidget::~MinimalStreamWidget() { @@ -61,22 +63,27 @@ void MinimalStreamWidget::init() { peakProgressBar.hide(); } -#define DECAY_STEP (1.0 / PEAKS_RATE) +void MinimalStreamWidget::stopDecay() { + if (decayTickId) { + remove_tick_callback(decayTickId); + decayTickId = 0; + } +} -void MinimalStreamWidget::updatePeak(double v) { - if (lastPeak >= DECAY_STEP) - if (v < lastPeak - DECAY_STEP) - v = lastPeak - DECAY_STEP; +void MinimalStreamWidget::updatePeak(double v, double decayStep) { + if (lastPeak >= decayStep) + if (v < lastPeak - decayStep) + v = lastPeak - decayStep; lastPeak = v; - if (v >= 0) { - peakProgressBar.set_sensitive(TRUE); - peakProgressBar.set_fraction(v); - } else { - peakProgressBar.set_sensitive(FALSE); - peakProgressBar.set_fraction(0); - } + if (v >= 0) { + peakProgressBar.set_sensitive(TRUE); + peakProgressBar.set_fraction(v); + } else { + peakProgressBar.set_sensitive(FALSE); + peakProgressBar.set_fraction(0); + } enableVolumeMeter(); } @@ -98,6 +105,40 @@ void MinimalStreamWidget::setVolumeMeterVisible(bool v) { peakProgressBar.show(); } } else { + stopDecay(); peakProgressBar.hide(); } } + +bool MinimalStreamWidget::decayOnTick(const Glib::RefPtr<Gdk::FrameClock>& frameClock) { + auto frameTime = frameClock->get_frame_time(); + + if (lastPeak == 0) { + decayTickId = 0; + return false; + } + + // Scale elapsed time (µs) so we decay in at most 1 second + if (frameTime != decayLastFrameTime) + updatePeak(0, (frameTime - decayLastFrameTime) / 1000000.0); + + decayLastFrameTime = frameTime; + + return true; +} + +void MinimalStreamWidget::decayToZero() { + if (decayTickId) + stopDecay(); + + auto frameClock = get_frame_clock(); + + if (!frameClock) { + /* Widget isn't visible, set all the way to 0 */ + updatePeak(0, 1.0); + return; + } + + decayLastFrameTime = frameClock->get_frame_time(); + decayTickId = add_tick_callback(sigc::mem_fun(*this, &MinimalStreamWidget::decayOnTick)); +} diff --git a/src/minimalstreamwidget.h b/src/minimalstreamwidget.h index 0f7275f..15a65e6 100644 --- a/src/minimalstreamwidget.h +++ b/src/minimalstreamwidget.h @@ -25,6 +25,7 @@ #include <pulse/pulseaudio.h> #define PEAKS_RATE 144 +#define DECAY_STEP (1.0 / PEAKS_RATE) class MinimalStreamWidget : public Gtk::Box { public: @@ -50,17 +51,22 @@ public: bool volumeMeterEnabled; void enableVolumeMeter(); - void updatePeak(double v); + void updatePeak(double v, double decayStep = DECAY_STEP); void setVolumeMeterVisible(bool v); + void decayToZero(); + void stopDecay(); + protected: /* Subclasses must call this after the constructor to finalize the initial * layout. */ void init(); + bool decayOnTick(const Glib::RefPtr<Gdk::FrameClock>& frame_clock); private : bool volumeMeterVisible; - + guint decayTickId; + gint64 decayLastFrameTime; }; #endif