The whole point of Thermal Canary Pro’s Smart Alert Overlay is that it’s silent until it isn’t: no gauge to babysit, just a toast that appears over your fullscreen game only when a sensor actually crosses a danger threshold, with a sound so you notice it even with a headset on.
That second part broke in a way that took real debugging to catch, because every signal said it was working.
The toast itself: bypassing the window manager on purpose
Getting a notification to draw on top of a fullscreen game on Linux isn’t free. Most windows are managed: the window manager decides stacking order, and a fullscreen game normally grabs the display and refuses to let anything else draw over it.
The overlay uses Qt.WindowType.BypassWindowManagerHint, which creates an X11 override-redirect
window, one the window manager has no say over at all. Combined with
WA_ShowWithoutActivating (so the toast never steals keyboard focus mid-game) and
WA_TranslucentBackground (for real rounded corners instead of a rectangular box), that’s
what makes a toast physically capable of appearing over a fullscreen Vulkan surface.
The bug: Qt swore the sound played, and nothing came out
For the actual alert sound, the natural choice was Qt’s own audio stack,
QSoundEffect or QMediaPlayer, both ship with PyQt6. Both reported a completely
successful playback lifecycle: QSoundEffect.isPlaying flipped from True to False
exactly like a normal finished playback, QMediaPlayer.mediaStatusChanged reached
EndOfMedia, zero errors surfaced anywhere.
And on a PipeWire desktop, nothing came out of the speakers. Not quiet, not distorted, just silent, while every API signal insisted the exact opposite.
The root cause, confirmed live and advisor-reviewed: PyQt6 bundles its own
libffmpegmediaplugin.so, which links its own copy of libpulsecommon. That bundled
copy appears to silently fail the shared-memory ring-buffer handshake against the
system’s running pipewire-pulse daemon.
The client believes it opened a stream and
happily writes audio samples into it, nobody on the other end is actually reading them.
Same WAV file, played via the system’s own paplay on the same machine at the same
moment, works instantly.
The fix: shell out, don’t fight Qt’s audio stack
Rather than trying to patch or replace Qt’s bundled audio plugin per user’s
Pulse/PipeWire/ALSA setup, the fix is to skip it entirely: fire-and-forget subprocess
calls to whichever of pw-play, paplay, or aplay actually exists on the system, in
that order. paplay gets volume control (--volume=<0-65536>), aplay is the last
resort with no volume control at all, for a pure-ALSA system with neither of the other
two installed.
All three use the system’s real audio libraries, not a bundled copy, so the same handshake that silently failed inside the app works normally outside it.
Why the alarm doesn’t stop when you dismiss the toast
One more deliberate, slightly aggressive design choice: closing the toast (clicking it, or its auto-dismiss timer for a WARN-level alert) does not stop the alarm sound for a CRIT-level one. CRIT loops a beep every 2.5 seconds for as long as the sensor genuinely stays in danger, and it keeps looping even if you’ve already closed the visual toast.
Dismissing a notification isn’t the same as the hardware actually cooling down, so the notification can go away without the alarm going quiet.
The loop only stops on its own once the sensor cools 2 degrees below the CRIT threshold, a small hysteresis margin so a sensor oscillating right at the line doesn’t restart the sound every single tick.
Why this matters beyond one app
If you’re building anything on Linux that needs to make noise reliably, audio players are one of the rare cases where shelling out to a real CLI tool beats a language’s own “native” audio bindings, precisely because the bindings bundle their own copy of a library the system already has a working one of.
Two copies of the same audio stack talking past each other is a specific, checkable failure mode, and Qt’s own success signals will not tell you it happened.
