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 000000000..f8dcb6ae1 Binary files /dev/null and b/Shaders/qsb/wave_spectrum.frag.qsb differ diff --git a/Widgets/AudioSpectrum/NWaveSpectrum.qml b/Widgets/AudioSpectrum/NWaveSpectrum.qml index 5ae371452..2fa0accf0 100644 --- a/Widgets/AudioSpectrum/NWaveSpectrum.qml +++ b/Widgets/AudioSpectrum/NWaveSpectrum.qml @@ -1,5 +1,5 @@ import QtQuick -import QtQuick.Shapes +import Quickshell import qs.Commons Item { @@ -14,115 +14,57 @@ Item { property bool showMinimumSignal: false property real minimumSignalValue: 0.05 // Default to 5% of height - // Safe degenerate-path fallback: valid off-screen line that renders nothing visible. - // Bare move-to paths like "M 0 0" can crash Qt's CurveRenderer triangulation. - readonly property string _safeFallbackPath: "M -1 -1 L -1 0" + readonly property int valuesCount: (values && Array.isArray(values)) ? values.length : 0 + readonly property bool hasData: valuesCount >= 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 } }