Files
qbz/crates/qbz-qt/qml/immersive/SpectralOverlay.qml
T
vicrodh e6c058c2f2 feat(qt): immersive shader scenes — RHI items, scene menu, GL link pairing
Port the immersive GPU scenes: Plasma, Tunnel, Aurora, Ambient, Spectral
Ribbon and Line Bed as QQuickRhiItems/ShaderEffects under a pulse-driven
ShaderSceneLayer, plus the Qt-only Tunnel Flow (Tauri Canvas2D port) fed
by the Viz16 stream. Scene rows live in the header view menu (no Off row);
g cycles the ring. A fresh immersive open defaults to Split > Lyrics
(owner ruling).

OpenGL link fix (owner smoke: Tunnel black with "uniform _22 declared as
type buf and type buf"): every ShaderEffect fragment with its own uniform
layout now ships a paired vertex stage declaring the identical block —
the shared spectrum.vert only ever linked under Vulkan.

RHI hardening: render pass descriptors on the ping-pong targets (the
Tunnel Flow segfault / Plasma dead window), mirrorVertically tied to the
backend (!== OpenGL), ribbon sub-uploads carry setSourceSize.

km
2026-08-16 08:22:57 -06:00

115 lines
4.0 KiB
QML

// Spectral Ribbon overlay (immersive scene mode 4) — green axes + labels +
// the stream header, drawn OVER the RibbonItem's GPU spectrogram. QML port
// of qbz-ui's ImmersiveSpectralOverlay.slint (B-block idiom: everything is
// positioned as FRACTIONS of the viewport so the axes grow/shrink with the
// window). The plot rectangle MUST match spectral_ribbon.frag
// (PLOT_X0..Y1); the overlay fractions are the Slint TOP-DOWN ones
// (0.070/0.780 = 1 - the shader's 0.930/0.220).
//
// No MouseArea anywhere — input passes through to the player bar below
// (the Slint overlay's "plain Rectangle, no TouchArea" rule).
import QtQuick
import com.blitzfc.qbz
Item {
id: root
anchors.fill: parent
// Plot rect — the Slint overlay's fractions (see the header).
readonly property real plotX0: width * 0.055
readonly property real plotX1: width * 0.970
readonly property real plotY0: height * 0.070
readonly property real plotY1: height * 0.780
readonly property real plotW: plotX1 - plotX0
readonly property real plotH: plotY1 - plotY0
readonly property color axis: "#5fb87a"
// Nyquist in kHz (delivered rate / 2). Falls back to 22.05 (44.1k)
// when nothing is reported yet (npEffRateHz 0).
readonly property real nyqKhz: QbzPlayer.npEffRateHz > 0
? QbzPlayer.npEffRateHz / 2000.0
: 22.05
readonly property int dur: QbzPlayer.npDurationSecs
// --- Stream header (top center, green box) ---------------------------
Rectangle {
// The immersive search cortinilla lives at the same height in the
// header row — while the search is open it wins the space (owner
// smoke 2026-08-15: they overlapped).
visible: !QbzImmersive.immSearchOpen
x: (root.width - width) / 2
y: root.height * 0.045
width: hdr.implicitWidth + 28
height: 22
radius: 5
border.width: 1
border.color: root.axis
color: "#e60a140d"
Text {
id: hdr
anchors.centerIn: parent
text: "Stream 1/1: Audio Stream, " + QbzPlayer.npEffRateHz + " Hz, "
+ QbzPlayer.npEffBits + " bits, FFT:4096, Bands:512"
color: root.axis
font.pixelSize: 12
}
}
// --- Axis frame (L-shape) --------------------------------------------
Rectangle { // vertical (frequency)
x: root.plotX0
y: root.plotY0
width: 1
height: root.plotH
color: root.axis
}
Rectangle { // horizontal (time)
x: root.plotX0
y: root.plotY1
width: root.plotW
height: 1
color: root.axis
}
// --- Frequency axis labels (Y, left) — 0 .. Nyquist ------------------
Repeater {
model: [0.0, 0.25, 0.5, 0.75, 1.0]
Text {
required property real modelData
x: root.plotX0 - 46
y: root.plotY1 - modelData * root.plotH - height / 2
width: 40
horizontalAlignment: Text.AlignRight
text: Math.round(root.nyqKhz * modelData) + "k"
color: root.axis
font.pixelSize: 11
}
}
// --- Time axis labels (X, bottom) — 30 s ticks -----------------------
Repeater {
model: 31
Text {
required property int index
visible: root.dur > 0 && (index * 30) <= root.dur
x: root.plotX0 + (root.dur > 0 ? root.plotW * ((index * 30) / root.dur) : 0) - width / 2
y: root.plotY1 + 6
text: Math.floor((index * 30) / 60) + ":"
+ ((index * 30) % 60 < 10 ? "0" : "") + ((index * 30) % 60)
color: root.axis
font.pixelSize: 10
}
}
// Final duration marker at the right edge.
Text {
visible: root.dur > 0
x: root.plotX1 - width / 2
y: root.plotY1 + 6
text: Math.floor(root.dur / 60) + ":"
+ (root.dur % 60 < 10 ? "0" : "") + (root.dur % 60)
color: root.axis
font.pixelSize: 10
}
}