Skip to content

Commit 993191a

Browse files
fuddlesworthruvnet
andcommitted
fix(audio): fix CAVA audio pipeline and harden bar count handling
CAVA requires even bar counts for stereo output but odd values could reach the process, causing silent exit code 1. The audioSpectrum QML binding also used || [] which forced V4 JS conversion, losing the native QVector<float> type needed by the C++ fast path. - Enforce even bar counts at all layers (CavaService, KCM setter, slider) - Move bar count constants to shared Audio::MinBars/MaxBars in constants.h - Use Binding element with `when` guard for audioSpectrum to avoid undefined hitting the slow QVariantList conversion path - Move exit diagnostics from stateChanged to finished signal per Qt API - Only warn on non-zero exit code (stderr on exit 0 is normal for CAVA) - Switch to SeparateChannels to capture CAVA stderr for diagnostics - Add static_assert for even MinBars/MaxBars at compile time Co-Authored-By: claude-flow <ruv@ruv.net>
1 parent 8e8c053 commit 993191a

File tree

6 files changed

+52
-12
lines changed

6 files changed

+52
-12
lines changed

kcm/kcm_plasmazones.cpp

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -814,8 +814,11 @@ void KCMPlasmaZones::setEnableAudioVisualizer(bool enable)
814814

815815
void KCMPlasmaZones::setAudioSpectrumBarCount(int count)
816816
{
817-
if (m_settings->audioSpectrumBarCount() != count) {
818-
m_settings->setAudioSpectrumBarCount(count);
817+
// CAVA requires even bar count for stereo output
818+
const int even = (count % 2 != 0) ? count + 1 : count;
819+
const int clamped = qBound(Audio::MinBars, even, Audio::MaxBars);
820+
if (m_settings->audioSpectrumBarCount() != clamped) {
821+
m_settings->setAudioSpectrumBarCount(clamped);
819822
Q_EMIT audioSpectrumBarCountChanged();
820823
setNeedsSave(true);
821824
}

kcm/ui/tabs/ZonesTab.qml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -413,9 +413,9 @@ ScrollView {
413413
Slider {
414414
id: audioBarsSlider
415415
Layout.preferredWidth: root.constants.sliderPreferredWidth
416-
from: 16
417-
to: 256
418-
stepSize: 1
416+
from: 16 // Audio::MinBars (src/core/constants.h)
417+
to: 256 // Audio::MaxBars (src/core/constants.h)
418+
stepSize: 2 // CAVA requires even bar count for stereo
419419
value: kcm.audioSpectrumBarCount
420420
onMoved: kcm.audioSpectrumBarCount = Math.round(value)
421421
}

src/core/constants.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,14 @@ inline constexpr QLatin1String AutoAssign{"autoAssign"};
158158
inline constexpr QLatin1String Colors{"colors"};
159159
}
160160

161+
/**
162+
* @brief Audio visualization constants (CAVA)
163+
*/
164+
namespace Audio {
165+
constexpr int MinBars = 16;
166+
constexpr int MaxBars = 256;
167+
}
168+
161169
/**
162170
* @brief D-Bus service constants
163171
*/

src/daemon/cavaservice.cpp

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,14 @@
77
#include <QStandardPaths>
88
#include <QFileInfo>
99

10+
#include "../core/constants.h"
1011
#include "../core/logging.h"
1112

1213
namespace PlasmaZones {
1314

1415
static constexpr int kAsciiMaxRange = 1000; // CAVA ascii_max_range
15-
static constexpr int kMinBars = 16;
16-
static constexpr int kMaxBars = 256;
16+
static_assert(Audio::MinBars % 2 == 0, "Audio::MinBars must be even for CAVA stereo");
17+
static_assert(Audio::MaxBars % 2 == 0, "Audio::MaxBars must be even for CAVA stereo");
1718

1819
CavaService::CavaService(QObject* parent)
1920
: QObject(parent)
@@ -52,15 +53,18 @@ void CavaService::start()
5253
this, &CavaService::onReadyReadStandardOutput);
5354
connect(m_process, &QProcess::stateChanged,
5455
this, &CavaService::onProcessStateChanged);
56+
connect(m_process, &QProcess::finished,
57+
this, &CavaService::onProcessFinished);
5558
connect(m_process, &QProcess::errorOccurred,
5659
this, &CavaService::onProcessError);
5760
}
5861

5962
m_stdoutBuffer.clear();
6063
m_spectrum.clear();
6164

62-
// Kurve-style: pass config via stdin, read raw output from stdout
63-
m_process->setProcessChannelMode(QProcess::ForwardedErrorChannel);
65+
// Kurve-style: pass config via stdin, read raw output from stdout.
66+
// Use SeparateChannels so we can capture stderr for error diagnostics.
67+
m_process->setProcessChannelMode(QProcess::SeparateChannels);
6468
m_process->start(QStringLiteral("sh"), QStringList{QStringLiteral("-c"),
6569
QStringLiteral("exec %1 -p /dev/stdin <<'CAVAEOF'\n%2\nCAVAEOF").arg(cavaPath, m_config)});
6670

@@ -90,7 +94,10 @@ bool CavaService::isRunning() const
9094

9195
void CavaService::setBarCount(int count)
9296
{
93-
const int clamped = qBound(kMinBars, count, kMaxBars);
97+
// CAVA requires even bar count for stereo output (bars split between L/R channels).
98+
// Round to even first, then clamp — ensures we never exceed MaxBars after rounding.
99+
int even = (count % 2 != 0) ? count + 1 : count;
100+
const int clamped = qBound(Audio::MinBars, even, Audio::MaxBars);
94101
if (m_barCount != clamped) {
95102
m_barCount = clamped;
96103
if (isRunning()) {
@@ -207,6 +214,19 @@ void CavaService::onProcessStateChanged(QProcess::ProcessState state)
207214
}
208215
}
209216

217+
void CavaService::onProcessFinished(int exitCode, QProcess::ExitStatus /*exitStatus*/)
218+
{
219+
if (m_stopping || m_pendingRestart) {
220+
return;
221+
}
222+
if (exitCode != 0) {
223+
const QByteArray stderrOutput = m_process ? m_process->readAllStandardError().left(500)
224+
: QByteArray();
225+
qCWarning(lcOverlay) << "CAVA exited with code" << exitCode
226+
<< "stderr:" << stderrOutput;
227+
}
228+
}
229+
210230
void CavaService::onProcessError(QProcess::ProcessError error)
211231
{
212232
// Suppress errors from intentional stop() or restartAsync() — SIGTERM causes QProcess::Crashed

src/daemon/cavaservice.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ class CavaService : public QObject
6363
void buildConfig();
6464
void onReadyReadStandardOutput();
6565
void onProcessStateChanged(QProcess::ProcessState state);
66+
void onProcessFinished(int exitCode, QProcess::ExitStatus exitStatus);
6667
void onProcessError(QProcess::ProcessError error);
6768
void restartAsync();
6869
static QString detectAudioMethod();

src/shared/ZoneShaderRenderer.qml

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,15 +44,23 @@ Item {
4444
iResolution: root.safeConfig.iResolution || Qt.size(width, height)
4545
iMouse: root.safeConfig.iMouse || Qt.point(0, 0)
4646

47-
audioSpectrum: root.safeConfig.audioSpectrum || []
48-
4947
onStatusChanged: {
5048
if (status === ZoneShaderItem.Error) {
5149
root.shaderError(errorLog)
5250
}
5351
}
5452
}
5553

54+
// Use Binding with `when` guard to avoid passing undefined to the C++ setter
55+
// when config is null. Without this, undefined hits the slow QVariantList path
56+
// in setAudioSpectrumVariant instead of preserving QVector<float> type identity.
57+
Binding {
58+
target: zoneShaderItem
59+
property: "audioSpectrum"
60+
value: root.safeConfig.audioSpectrum
61+
when: root.safeConfig.audioSpectrum !== undefined
62+
}
63+
5664
Binding {
5765
target: zoneShaderItem
5866
property: "labelsTexture"

0 commit comments

Comments
 (0)