MilkDrop visualisation on Hyprland, wired to whatever is currently playing.
It captures audio from the PipeWire monitor source of the default sink, so it visualises any application — a music player, a browser tab, anything. Bound to SUPER+M as a toggle.
Built around projectm-pulseaudio 3.1.12 on Arch/CachyOS. Most of this repo
exists to work around undocumented behaviour in that version; see
Findings.
git clone https://github.com/maribotto/projectm-setup ~/projectm-setup
cd ~/projectm-setup
./install.shinstall.sh symlinks bin/ into ~/.local/bin and copies config/ into
~/.config/projectM, rewriting the absolute paths for your home directory.
Existing configs are backed up first.
Scripts are symlinked, not copied, so this directory stays the single source of truth — edit here and the change takes effect immediately.
Hyprland settings are not installed automatically; they belong in your own
config. See hypr/snippets.lua.
projectm-pulseaudio |
the visualiser itself (pacman -S projectm-pulseaudio) |
| PipeWire or PulseAudio | pactl is used to find the monitor source |
| Hyprland | optional; only for the window rules and the idle-preset workaround |
bin/projectm-vis |
launch/close, audio source, first preset |
bin/projectm-playlist |
builds the .prjm playlist from the preset tree |
config/config.inp |
projectM's own settings (mesh, fps, duration) |
config/qprojectM.conf |
Qt frontend — the playlist path lives here |
config/qprojectM-pulseaudio.conf |
audio device |
hypr/snippets.lua |
window rules and keybinding |
Presets live in ~/.local/share/projectM/presets/:
presets/
├── system -> /usr/share/projectM/presets 4188 (managed by the package manager)
├── milkdrop-original/ 552 (git)
└── en-d/ 40 (git)
Add more from the projectM organisation:
cd ~/.local/share/projectM/presets
git clone --depth 1 https://github.com/projectM-visualizer/presets-en-d en-d
git clone --depth 1 https://github.com/projectM-visualizer/presets-milkdrop-original milkdrop-originalNesting depth does not matter — the playlist generator walks the whole tree. The playlist is rebuilt on every launch, so changes take effect by themselves.
presets-cream-of-the-crop (9795 presets) is deliberately not included.
Its MilkDrop 2 presets lean heavily on HLSL shaders that projectM 3.1.12 only
partially supports, so many render wrong or black — and on its own it exceeds
the crash threshold described below.
~/.local/share/projectM/textures/ holds 67 images from
presets-milkdrop-texture-pack.
projectM looks for textures in a hardcoded path, /usr/share/projectM/textures.
config.inp has no key for a texture path at all, so a symlink is the only
option. Run once:
sudo ln -s ~/.local/share/projectM/textures /usr/share/projectM/texturesCareful: if that path already exists, ln -s follows it and creates the
link inside the target, producing a symlink loop that projectM's
FileScanner will follow (it uses FTS_LOGICAL). Check with
ls -la /usr/share/projectM/.
A symlink is the right shape here because the package manager does not own it and so will not remove it on upgrade.
None of the following is documented upstream. Each was established by reading the projectM 3.1.12 source or by measurement.
config.inp's Preset Path is overridden. The Qt frontend stores its own
path in PlaylistFile in qprojectM.conf, and that wins. The file is created
on first run and freezes whatever path was current at that moment.
The Qt frontend reads only the top level of the directory it is given. Point it at a tree of preset packs and the playlist comes up empty, with the built-in idle preset on screen.
This is why bin/projectm-playlist exists: it walks the whole tree, following
symlinks, and writes a .prjm playlist that PlaylistFile points at.
A consequence worth knowing: /usr/share/projectM/presets has 6 native .so
presets in its root and zero .milk files. On a default install those six
are the only presets in rotation — the package's 4188 MilkDrop presets are
never loaded.
projectM exits silently at startup, with no error message, if the playlist is too long. Bisected: 5000 entries load fine, 8000 crash.
projectm-playlist warns when the count goes over 5000.
At startup projectM loads a built-in idle preset —
idle://Geiss & Sperl - Feedback (projectM idle HDR mix).milk, the M logo with
headphones. It is not on the playlist, and Shuffle Enabled has no effect on
it, because shuffle only governs transitions.
It is replaced only once Preset Duration elapses, 30 seconds by default.
projectm-vis works around this by sending CTRL-R (random preset) as soon as
the window appears, via Hyprland's send_shortcut dispatcher. Without
Hyprland, press CTRL-R yourself.
TextureManager's constructor scans datadir + "/presets",
datadir + "/textures" and the preset path at startup, and loads everything it
finds into memory without logging anything.
The Located texture / Failed to locate texture lines come from a fallback
path used only for names the scan did not find. Silence means the textures
were found.
(projectM 3.1.12 has a bug here: Failed to locate texture is printed after a
successful load too, because the break is not followed by a return. Do not
read that log line literally.)
All three are worked around in projectm-vis:
- The process name is truncated to 15 characters (
projectM-pulsea), sopkill -x projectM-pulseaudionever matches. Match the command line instead. - projectM ignores SIGTERM. Measured: the process was still alive 134 s
after
timeout 12sent one. Closing it requires SIGKILL. pkill -f projectM-pulseaudiokills your own shell if that string appears on its command line. Do not run it interactively by hand.
In config/config.inp:
| key | value | note |
|---|---|---|
Mesh X / Mesh Y |
220 / 125 | resolution of the per-pixel equation mesh. Heavy at 4K; 128/72 is about a third of the load |
FPS |
60 | appears to do nothing — see below |
Preset Duration |
30 | seconds per preset |
Shuffle Enabled |
true | already the built-in default; present for visibility |
Preset Path |
— | unused, see Findings |
On FPS: at 5 the CPU load measured 23–25%, at 60 it measured 17–21% — the
wrong direction. FPS is a timing parameter inside projectM, not a frame
limiter.
projectM's own, while the window has focus:
CTRL-R |
random preset |
CTRL-N / CTRL-P |
next / previous |
CTRL-Y |
toggle shuffle |
CTRL-L |
lock the current preset |
CTRL-F |
fullscreen |
CTRL-M |
show / hide the menu |
CachyOS, Hyprland 0.56.1, Wayland, PipeWire 1.6.8, projectm-pulseaudio
3.1.12, Intel Arc Pro 130T/140T, 3840x2160 @ 60 Hz at scale 2.
libprojectm 4.1.6 is installed on that system but unused — there is no
packaged frontend for the 4.x series, and the 3.x frontends link against
libprojectM.so.3.
MIT, see LICENSE.
Presets and textures are not part of this repository; they carry their own licenses from the projectM organisation.