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
This commit is contained in:
Arun Raghavan 2025-06-06 11:50:12 +05:30
parent cd7bd578ae
commit ce48aec45f
3 changed files with 88 additions and 23 deletions

View file

@ -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);
}
}
}

View file

@ -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));
}

View file

@ -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