ywp is a lightweight wallpaper-based music visualizer designed to integrate the power of cava with the Wayland Layer Shell Protocol.
circular |
spline |
|---|---|
![]() |
![]() |
cava is a powerful piece of software that supports a variety of audio backends. It dynamically captures audio waveforms in real time, applies FFT via FFTW, and exposes the processed data for visualization. It even has SDL support for shader-based rendering.
However, for my purposes, I wanted the visualizer to run directly in the background of my Wayland compositor (Hyprland), and setting this up cleanly is not straightforward.
Hyprland does provide ways to set windows as your background—such as hyprwinwrap. While it works well for turning arbitrary windows into wallpapers, it usually consumes more resources than necessary for something as simple as a visualizer.
Another option is to rely on your desktop environment’s tools. In my case, that was Quickshell, which is built on top of QtQuick. You can build a simple music visualizer using QtQuick’s shader system or other elements in the API (see Appendix A). However, there are two major issues with this approach:
- Shader Buffer Objects (SBOs) cannot be passed, and uniform arrays are not supported—meaning neither dynamic nor static arrays are available.
- Communication with
cavarelies on parsing raw output, which is unreliable and unnecessarily inefficient.
Other projects, such as astal, integrate cava as a library and expose it as a service (astal-cava). But since I’m not using astal as my compositor, that solution does not fit my setup well.
The goal of this project is therefore to provide a minimal, performant, compositor-agnostic (Wayland for now; see Roadmap) background music visualizer that anyone can easily integrate into their system.
If you're using the Nix package manager, you can run the project with:
nix run github:yunusey/ywpywp runs exclusively on Linux. You will need the following dependencies:
libffilibGLUegl-waylandlibxkbcommonfftwpulseaudioxxd(required only when compiling from source; not needed when running the binary—see Shaders)
If you want to use an audio backend other than PulseAudio, you may run into issues for now. The audio libraries are linked at compile time, and I have not yet implemented full support for alternatives. You can try compiling with the appropriate flags (e.g., -DCAVA_INPUT_ALSA=ON)—it may or may not work. I plan to add and test support for additional backends soon.
Shaders are located in the ./shaders directory. At compile time, cmake runs xxd on all shader files in this directory, producing variables of the form:
unsigned char shaders_{name}_{type}[]unsigned char shaders_{name}_{type}_len
For example: shaders_spline_frag and shaders_spline_frag_len.
These variables are declared as extern in src/shader.h and included in src/shader.c so that they are correctly linked at build time.
Currently, ywp does not support loading shaders at runtime—but this is a feature I’d like to add in the future.
- X11 support
- Support for ALSA, FIFO, and other Linux audio backends
- Mouse and keyboard interactions
- Multi-monitor support
Warning
The Wayland Layer Shell Protocol is feature-rich. While ywp currently does not support input handling, the protocol does allow both mouse and keyboard interaction. Mouse input should be relatively easy to integrate; keyboard input is considerably trickier and adds unnecessary complexity at this stage.
If you want keyboard input in your own layer-shell-based project, I recommend taking a look at my Raylib fork, which adds wlrlayer as a Raylib platform. It works well, but the complexity is outside the scope of ywp, so it's not included by default. I plan to include an example demonstrating how to use Raylib to draw surfaces via Layer Shell in the future.
To begin, create a Singleton that communicates with cava:
pragma Singleton
import qs.config
import Quickshell
import Quickshell.Io
import QtQuick
Singleton {
id: root
property list<int> values: Array(Appearance.cava.visualizerBars)
Process {
id: cavaProc
running: Appearance.cava.enabled
command: ["sh", "-c", `printf '[general]\nframerate=${Appearance.cava.frameRate}\nwidth=100\nbars=${Appearance.cava.visualizerBars}\nsleep_timer=3\n[output]\nchannels=mono\nmethod=raw\nraw_target=/dev/stdout\ndata_format=ascii\nascii_max_range=100' | cava -p /dev/stdin`]
stdout: SplitParser {
onRead: data => {
root.values = data.slice(0, -1).split(";").map(v => parseInt(v, 10));
}
}
}
}Next, use the values from this Singleton in your QML widget. One possible implementation is:
pragma ComponentBehavior: Bound
import qs.widgets
import qs.config
import qs.services
import Quickshell
import QtQuick
import QtQuick
import QtQuick.Shapes
Item {
id: root
property bool enabled: false
readonly property list<int> values: Cava.values
visible: root.enabled
anchors.fill: parent
Variants {
id: visualizer
model: Array.from({
length: root.enabled ? Cava.values.length : 0
}, (_, i) => i)
delegate: PathCurve {
required property int modelData
property int index: modelData
property int value: Math.min(Math.max(0, Cava.values[index]), 100)
x: index / (Cava.values.length - 1) * root.width
y: root.height * (1. - value / 100.)
}
}
PathLine {
id: path_start
x: 0
y: visualizer.instances[0].y
}
PathLine {
id: path_end
x: root.width
y: root.height
}
Shape {
id: shape
preferredRendererType: Shape.GeometryRenderer
ShapePath {
id: path_curves
strokeColor: Colors.palette.text
strokeWidth: 5
capStyle: ShapePath.FlatCap
joinStyle: ShapePath.MiterJoin
startX: 0
startY: root.height
fillColor: Qt.alpha(Colors.palette[Colors.flavour], 0.2)
pathElements: [path_start, ...visualizer.instances, path_end]
}
}
}This produces an effect visually similar to the spline shader.
Caution
My setup includes a custom qs.config file with an Appearance dictionary containing visualizerBars and frameRate. Replace these with appropriate values for your configuration.


