vsp is a lightweight OpenGL audio visualizer, which captures your system audio and displays the spectrum. The yellow-on-black colour scheme is a part of its heritage. It's a port of rvsp, but improved in many ways.
CinematicVSP.webm
- Meson (build system)
- CMake (for Meson; more specifically, for KissFFT)
- GLFW 3.0
- PipeWire ≥0.3
$ git clone --recurse-submodules https://github.com/cynthia2006/vsp
$ cd vsp
$ meson setup builddir --buildtype=release
$ meson compile -C builddir
The final artifact would be vsp in builddir—takes no options, runs out of the box.
- ↑ to increase and ↓ to decrease gain of the spectrum.
- ← to decrease and → to increase smoothing time constant (0 < τ < 1).
This app is as bare minimum as it could get, and the defaults are universally the best choice. However, if your needs are special you can of course adjust the options by editing the code itself (options in vsp.c). There is no mechanism for loading configuration files, as the code needed for that would alone outweigh the existing codebase. In the Linux community this is known as the "suckless" approach.
Smoothing time constant (τ) is a parameter controlling temporal smoothing of spectrum; higher the values the smoother the animation. The default is 0.8, which is quite eye-pleasing; lower values (typically around 0.6) are good for high-BPM music if you're into that.
A Mel-scale spectrum (20-20000 Hz) is used to display lower frequencies in greater detail, and higher frequencies in coarse detail. Amplitude is in linear scale however, simply because it's visually appealing. You usually adjust the gain of the spectrum using the ↑ and ↓ keys as needed (e.g. the volume of music is too low).
Rust is not the magic bullet. A polished turd is still a turd; so no matter how much safety it tries to guarantee, the programmer is still liable for erroneous logic. Welcome to Rust bindings. “Fearlessness” in Rust is often taken for granted, and relying on these assumptions with binding crates turns out to be — more often than not — catastrophic, especially if the documentation is sparse.
C interoperability in Rust is objectively poor, and careless development could result in errors that are hard to identify. Not all bindings have the same amount of test coverage or code quality, and if the crate is not popular enough, the user of the binding is then left with a mere “illusion of safety”, reportedly more dangerous than dealing with the unsafe interface directly because the user isn’t aware. When a malfunction occurs, the user is forced to read all the source code to identify what went wrong. The result? A huge time waste.
No abstraction is technically zero-cost, even if the overhead is negligible. Bindings should mirror the C API in a one-to-one fashion as much as possible, even if it's not aligned with the idioms of the language. Making spurious allocations on behalf, incomplete coverage, use of suboptimal algorithms introduce a tradeoff between ease of use and performance.
This project was abandoned because of pipewire-rs — a load of nonsense. PipeWire is fundamental to its design, so it couldn’t traded for anything else. Though PulseAudio could be used, it’s widely considered a legacy in the modern Linux desktop.
Another reason to cease development was for the complexities of winit and glutin — all to initialise an OpenGL context. A comprehensive tutorial for these two libraries didn’t exist, and the only learning resource was a 600-700 line example to draw an triangle in a portable manner across devices. Those two libraries were intended to be building blocks for glium, which although fairly simple to work with, has been abandoned since 2016, because of inconsistencies of OpenGL implementation across GPU vendors.
rvsp begun in December 2022, as a revival of an even older project dating back to 2019. Initially, it used SDL_Renderer for drawing non-AA lines. The development was shortly abandoned. In late 2024, parts of wv-renderer were factored into this project; it used skia-python but with the software rasterizer. Since the focus been on real-time visualisation, Skia’s Ganesh was leveraged to accelerate rendering; was tiresome.
Skia had been a heavy compile-time dependency for doing something as simple as drawling a polygon, and most of its functionalities weren’t utilised at all. It was phased out, and OpenGL was used directly with 8x MSAA. OpenGL at its core is a C library. As unsafe Rust is treated like unsafe sex, glium was sought to be the alternative. At that point, the project still relied upon SDL, but glium had seamless integration with the winit + glutin pair. To integrate it with SDL, the required traits with albeit confusing mechanics, were implemented.
From the start, the project intended to capture the system audio, but SDL’s functionality to capture monitor devices was limited to PulseAudio. pavucontrol/qpwgraph had been used to set the program’s input device to the system sink monitor; it was bothersome, and unreliable. Eventually, SDL was phased out. The stack comprised of PipeWire, winit + glutin, and glium. The decision of breaking cross-platform compatibility was deliberate as the project was primarily developed on Linux. Eventually, glium was phased out, and OpenGL code was revived, but the window creation and context initialisation code outgrew the actual logic of the program, which was as simple as it could get. The real struggles began from this point onward.
pipewire-rs was unable to guarantee feature parity with PipeWire’s C API. The support for thread-loops was severely broken, causing non-deterministic programs crashes. This implied that PipeWire's event-loop was unable to run parallel to the main thread blocked with winit's event-loop. PipeWire's event-loop would have to be iterated either in the about_to_wait() callback of winit's event-loop, or when event loop's file descriptor (ref.) was read-ready. There had been a problem with the first approach: if winit's event-loop waited indefinitely for an event to arrive, it would stall PipeWire's event-loop. Though the second option was pursued, winit didn’t expose a method to do so. PipeWire's event-loop, however, had the add_io() method, so an inversion of control had to be done. Although this architectural issue was resolved, another issue couldn’t be, where mono-channel audio was requested, but the version of libpipewire linked by pipewire-rs didn’t downmix the the stereo signal, only connecting the left-channel of the monitor device, so the downmixing logic had to be implemented manually. This wasn’t an issue with the C API, and PipeWire’s principal developer couldn’t reason about this malfunction. Therefore with all things considered, it was abandoned and rewritten from scratch in C.
All that glitters is not gold. C might not be blazingly-fast™, have fearless concurrency, or memory safety, but it has stood the test of time, and has achieved immortality. It wasn’t driven by hype; people naturally wrote C, and will continue to do so for decades to come!