From e4593216f0ce8009816f9706c75e1427347eea7d Mon Sep 17 00:00:00 2001 From: Lemmy Date: Mon, 9 Mar 2026 11:55:35 -0400 Subject: [PATCH] fix(audio): replace NWaveSpectrum CurveRenderer with shader to fix SIGSEGV crash --- Shaders/frag/wave_spectrum.frag | 74 ++++++++++++ Shaders/qsb/wave_spectrum.frag.qsb | Bin 0 -> 3686 bytes Widgets/AudioSpectrum/NWaveSpectrum.qml | 144 +++++++----------------- 3 files changed, 117 insertions(+), 101 deletions(-) create mode 100644 Shaders/frag/wave_spectrum.frag create mode 100644 Shaders/qsb/wave_spectrum.frag.qsb diff --git a/Shaders/frag/wave_spectrum.frag b/Shaders/frag/wave_spectrum.frag new file mode 100644 index 000000000..c3de02d14 --- /dev/null +++ b/Shaders/frag/wave_spectrum.frag @@ -0,0 +1,74 @@ +#version 450 + +layout(location = 0) in vec2 qt_TexCoord0; +layout(location = 0) out vec4 fragColor; + +layout(binding = 1) uniform sampler2D dataSource; + +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + float qt_Opacity; + vec4 fillColor; + float count; + float texWidth; + float vertical; +}; + +// Sample amplitude from data texture (R channel) +float fetchData(float idx) { + float i = clamp(idx, 0.0, texWidth - 1.0); + float u = (floor(i) + 0.5) / texWidth; + return texture(dataSource, vec2(u, 0.5)).r; +} + +// Cubic Hermite interpolation for smooth wave curves +float cubicHermite(float y0, float y1, float y2, float y3, float t) { + float m1 = (y2 - y0) * 0.25; + float m2 = (y3 - y1) * 0.25; + float t2 = t * t; + float t3 = t2 * t; + return (2.0 * t3 - 3.0 * t2 + 1.0) * y1 + + (t3 - 2.0 * t2 + t) * m1 + + (-2.0 * t3 + 3.0 * t2) * y2 + + (t3 - t2) * m2; +} + +// Evaluate interpolated amplitude at fractional data index +float evalCurve(float dataIdx) { + float i = floor(dataIdx); + float t = dataIdx - i; + return cubicHermite( + fetchData(i - 1.0), + fetchData(i), + fetchData(i + 1.0), + fetchData(i + 2.0), + t + ); +} + +void main() { + vec2 uv = qt_TexCoord0; + + // Swap axes for vertical mode + float axisPos = (vertical > 0.5) ? uv.y : uv.x; + float crossPos = (vertical > 0.5) ? uv.x : uv.y; + + // Mirror: value[0] at center, value[count-1] at edges + float distFromCenter = abs(axisPos - 0.5) * 2.0; + float dataIdx = distFromCenter * max(count - 1.0, 1.0); + + // Interpolated amplitude, clamped to valid range + float amplitude = clamp(evalCurve(dataIdx), 0.0, 1.0); + + // Wave fills center ± amplitude/2 in the cross axis + float halfAmp = amplitude * 0.5; + float distFromMid = abs(crossPos - 0.5); + + // Antialiased edge (~1px smooth transition) + float edge = fwidth(crossPos) * 1.5; + float mask = smoothstep(halfAmp + edge, halfAmp - edge, distFromMid); + + // Premultiplied alpha output + float a = mask * fillColor.a; + fragColor = vec4(fillColor.rgb * a, a) * qt_Opacity; +} diff --git a/Shaders/qsb/wave_spectrum.frag.qsb b/Shaders/qsb/wave_spectrum.frag.qsb new file mode 100644 index 0000000000000000000000000000000000000000..f8dcb6ae1c0ec05259b47ff148a6411b59f17f3a GIT binary patch literal 3686 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 2 - // Reactive path that updates when values change - readonly property string svgPath: { - if (!values || !Array.isArray(values) || values.length === 0) { - return _safeFallbackPath; - } + // Data texture: one pixel per value, R channel = amplitude + Item { + id: dataRow + width: Math.max(root.valuesCount, 4) + height: 1 - if (!isFinite(width) || !isFinite(height) || width <= 0 || height <= 0) - return _safeFallbackPath; + Repeater { + model: dataRow.width - // Apply minimum signal if enabled - const processedValues = showMinimumSignal ? values.map(v => v === 0 ? minimumSignalValue : v) : values; - - // Create the mirrored values - const partToMirror = processedValues.slice(1).reverse(); - const mirroredValues = partToMirror.concat(processedValues); - - if (mirroredValues.length < 2) { - return _safeFallbackPath; - } - - const count = mirroredValues.length; - - for (let i = 0; i < count; i++) { - if (!isFinite(mirroredValues[i])) - return _safeFallbackPath; - } - - if (vertical) { - const stepY = height / (count - 1); - const centerX = width / 2; - const amplitude = width / 2; - - if (!isFinite(stepY) || !isFinite(centerX) || !isFinite(amplitude)) - return _safeFallbackPath; - - let xOffset = mirroredValues[0] * amplitude; - if (!isFinite(xOffset)) - return _safeFallbackPath; - let path = `M ${centerX - xOffset} 0`; - - for (let i = 1; i < count; i++) { - const y = i * stepY; - xOffset = mirroredValues[i] * amplitude; - if (!isFinite(y) || !isFinite(xOffset)) - return _safeFallbackPath; - path += ` L ${centerX - xOffset} ${y}`; + Rectangle { + required property int index + x: index + width: 1 + height: 1 + color: { + if (index >= root.valuesCount) + return Qt.rgba(0, 0, 0, 1); + var v = root.values[index]; + if (v === undefined || v === null || !isFinite(v)) + v = 0; + if (root.showMinimumSignal && v === 0) + v = root.minimumSignalValue; + return Qt.rgba(Math.max(0, Math.min(1, v)), 0, 0, 1); + } } - - for (let i = count - 1; i >= 0; i--) { - const y = i * stepY; - xOffset = mirroredValues[i] * amplitude; - if (!isFinite(y) || !isFinite(xOffset)) - return _safeFallbackPath; - path += ` L ${centerX + xOffset} ${y}`; - } - - return path + " Z"; - } else { - const stepX = width / (count - 1); - const centerY = height / 2; - const amplitude = height / 2; - - if (!isFinite(stepX) || !isFinite(centerY) || !isFinite(amplitude)) - return _safeFallbackPath; - - let yOffset = mirroredValues[0] * amplitude; - if (!isFinite(yOffset)) - return _safeFallbackPath; - let path = `M 0 ${centerY - yOffset}`; - - for (let i = 1; i < count; i++) { - const x = i * stepX; - yOffset = mirroredValues[i] * amplitude; - if (!isFinite(x) || !isFinite(yOffset)) - return _safeFallbackPath; - path += ` L ${x} ${centerY - yOffset}`; - } - - for (let i = count - 1; i >= 0; i--) { - const x = i * stepX; - yOffset = mirroredValues[i] * amplitude; - if (!isFinite(x) || !isFinite(yOffset)) - return _safeFallbackPath; - path += ` L ${x} ${centerY + yOffset}`; - } - - return path + " Z"; } } - Shape { - id: shape + ShaderEffectSource { + id: dataTex + sourceItem: dataRow + textureSize: Qt.size(dataRow.width, 1) + live: true + smooth: false + hideSource: true + } + + ShaderEffect { anchors.fill: parent - preferredRendererType: Shape.CurveRenderer - containsMode: Shape.FillContains + visible: root.hasData && root.width > 0 && root.height > 0 - ShapePath { - id: shapePath - fillColor: root.fillColor - strokeColor: root.strokeWidth > 0 ? root.strokeColor : "transparent" - strokeWidth: root.strokeWidth + property variant dataSource: dataTex + property color fillColor: root.fillColor + property real count: root.valuesCount + property real texWidth: dataRow.width + property real vertical: root.vertical ? 1.0 : 0.0 - PathSvg { - path: root.svgPath - } - } + fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/wave_spectrum.frag.qsb") + blending: true } }