Play sounds for desktop notifications that do not include one.
On GNOME (and other desktop environments built on
org.freedesktop.Notifications), a notification only makes a sound when the
app that sent it explicitly includes a sound-name or sound-file hint.
Most apps never do — Warp, terminals, IDE builds, cron-like tools — so their
notifications stay silent.
NotifySound is a small daemon that watches the notification bus and plays a sound of your choice whenever a notification arrives without its own sound. It also ships a simple GTK4 app to configure everything.
Install (per-user, enables autostart):
git clone https://github.com/ChristianM023/notify-sound.git && cd notify-sound && ./install.shUbuntu/Debian? Install the release package without cloning the repository.
- Python 3.10+
- PyGObject (
python3-gi) canberra-gtk-play(gnome-session-canberra)- GTK 4.10+ (for the GUI only)
dbus-monitor(part of thedbuspackage, present on every desktop)
Install on Ubuntu/Debian:
sudo apt install python3-gi gir1.2-gtk-4.0 gnome-session-canberra dbusgit clone https://github.com/ChristianM023/notify-sound.git
cd notify-sound
./install.shThis installs to ~/.local/bin, adds a launcher and enables autostart
(the daemon starts with your session). Use --no-autostart to skip that.
A custom PREFIX=/some/path ./install.sh installs elsewhere; the wrapper
exports NOTIFY_SOUND_BIN so autostart and the GUI always use the right
binary.
Ubuntu/Debian users can install the release package without cloning the repository:
sudo dpkg -i ./notify-sound_0.1.10_all.deb
sudo apt-get -f install
notify-soundThe package installs the program under /usr, does not enable a daemon
automatically, and includes a user-level systemd unit template:
mkdir -p ~/.config/systemd/user
cp /usr/share/notify-sound/notify-sound.service ~/.config/systemd/user/
systemctl --user enable --now notify-soundmkdir -p ~/.config/systemd/user
cp ~/.local/share/notify-sound/notify-sound.service ~/.config/systemd/user/
systemctl --user enable --now notify-soundFor a custom PREFIX, copy the generated unit from
$PREFIX/share/notify-sound/notify-sound.service instead.
rm -rf ~/.local/share/notify-sound ~/.local/bin/notify-sound
rm ~/.local/share/applications/notify-sound.desktop
rm ~/.config/autostart/notify-sound.desktop
rm -rf ~/.config/notify-soundAfter installing, verify it works in seconds:
notify-sound --daemon
notify-send "Test" "If you hear a sound, it works"If you installed with autostart enabled (the default), the daemon is already
running and the notify-send line alone is enough.
NotifySound runs a small daemon that listens to the desktop notification bus. When a notification arrives without a sound of its own, it plays the sound you configured in the GUI.
Data flow of a notification:
notification bus (org.freedesktop.Notifications / org.gtk.Notifications)
│
▼
dbus-monitor (eavesdrop=true)
│
▼
parser ──► app name resolution (desktop-entry → comm → synonym → app_name)
│
▼
play rules ──► enabled? → suppress-sound? → per-app? → own sound (no config)? → play
│
▼
player ──► canberra-gtk-play (OGG/WAV/FLAC) → fallback (gst → ffplay → mpv → mpg123)
The daemon only reads notification metadata and never stores the message body.
Notifications that already carry their own sound are left untouched, and
notifications that declare suppress-sound — apps that manage their own
audio, such as Chromium-based browsers for media — stay silent on purpose.
Apps that send notifications are collected automatically so you can tune each
one individually (enable/disable, per-app sound).
Technical details (parser contract, daemon lifecycle) live in the Development section.
| Level | Environments |
|---|---|
| Supported and tested | GNOME (validated on Ubuntu/Debian) |
| Compatible in theory, not tested yet | Other freedesktop desktops: KDE, XFCE, Cinnamon, MATE |
| Not supported | Non-Linux (Windows, macOS) |
NotifySound follows the freedesktop notification spec, so other desktops built
on org.freedesktop.Notifications should work, but they have not been
validated yet — nothing is claimed until it is tested.
notify-sound # open the settings GUI
notify-sound --daemon # start the daemon
notify-sound --quit # stop the daemon- Master on/off switch
- Autostart toggle: start the daemon with your session (writes/removes
~/.config/autostart/notify-sound.desktop) - Global sound picker (theme sounds from the active sound theme) + Test button
- Add any number of custom sound files; they appear in the global and per-app sound pickers (the list is capped to 3 visible rows with scroll)
- Apps that send their own sound are detected and marked with a "Tiene sonido propio" label; their per-app switch is off by default (enable it to hear both sounds on purpose)
- Per-app enable/disable, per-app sound selection and per-app Test button
- App aliasing: rename detected apps to a friendly display name ("AIMP" instead of the lowercase process id) via the "Renombrar" button
- App fusion: renaming an app to an alias already in use opens a confirmation popover to merge the two entries; the duplicate becomes a synonym and its future notifications are attributed to the survivor
- App info popover: per-app "Información" button shows the raw notification name, display alias, detected process, synonym count, notification count and last-seen time; synonyms can be restored to separate entries from here
- App list management: remove individual apps or "Vaciar lista" to reset all detected apps and their configuration
- App sorting: by arrival (default), by name, or by notification count
- Start/stop daemon buttons with live status (the GUI is single-instance: launching it again focuses the existing window)
-
OGG, WAV, FLAC: played with
canberra-gtk-play(no extra software). -
MP3, M4A, AAC, etc.:
canberra-gtk-playcannot decode them, so NotifySound falls back togst-launch-1.0(GStreamer), thenffplay,mpvormpg123. Install at least one of them, e.g.:sudo apt install gstreamer1.0-plugins-base gstreamer1.0-plugins-good
Stored in ~/.config/notify-sound/config.json:
{
"enabled": true,
"sound": "message",
"custom_sounds": ["/path/to/my-sound.mp3"],
"autostart": true,
"apps": {
"warp": { "enabled": true, "sound": null },
"aimp": {
"enabled": true,
"sound": null,
"name": "AIMP",
"synonyms": ["Canción A", "Canción B"]
}
}
}"sound" is the current choice: a theme sound id (message,
dialog-warning, ...) or the path of one of the files in custom_sounds.
"sound": null inside an app means "inherit the global sound". The
optional "name" field is a display alias for the auto-detected app list
(for example to show "AIMP" instead of the lowercase desktop-entry id);
it is editable from the GUI's "Renombrar" button and stays bounded by
MAX_APP_NAME_LENGTH. The optional "synonyms" list (bounded by
MAX_SYNONYMS = 64) records raw app_name values that should be
attributed to this entry — populated when you fuse two entries from the
GUI (renaming one to the other's alias) and used by the daemon to
canonicalize future notifications from those sources.
~/.config/notify-sound/state.json holds the auto-detected app list and
per-app metadata:
{
"apps_seen": ["aimp", "warp", "Telegram Desktop"],
"app_meta": {
"aimp": { "seen_count": 12, "last_seen": 1777000000.5, "comm": "aimp" }
}
}The app list is auto-detected, so entries are re-added when the app sends
a new notification. app_meta powers the "Información" popover (count,
last-seen time, detected process) and is written by the daemon; it is
optional and tolerated when absent (v0.1.7 state files still load).
NotifySound processes everything locally: it never connects to the network
and sends nothing to any server — no telemetry. The daemon reads only
metadata (app name, hints) from the notification bus and keeps it in memory;
the body of a notification is never stored. The only per-app data written to
disk is app_meta in ~/.config/notify-sound/state.json (notification
count, last-seen time, detected process) — user configuration and state,
never the content of a notification.
To observe notifications, the daemon runs dbus-monitor in eavesdrop mode
(eavesdrop=true), the only reliable way to watch the notification bus from
Python. NotifySound handles no secrets: no tokens, credentials or API keys,
so there is no surface for secret leaks. Config and state files are written
atomically (temp file + rename) with O_NOFOLLOW and mode 0600, so only
your user can read them. The instance lock file
($XDG_RUNTIME_DIR/notify-sound.pid.lock, per-user fallback in
~/.cache/notify-sound/) is opened the same way, with O_NOFOLLOW and mode
0600.
- Nothing plays at all: make sure
canberra-gtk-playis installed and test it withcanberra-gtk-play -i message. Verify the daemon is running withnotify-sound --quit(it reports "not running") or check the GUI status. - Custom sound file (MP3, M4A...) does not play:
canberra-gtk-playonly decodes OGG/WAV/FLAC. For other formats NotifySound falls back togst-launch-1.0(GStreamer), thenffplay,mpvormpg123— install at least one of them, e.g.sudo apt install gstreamer1.0-plugins-base gstreamer1.0-plugins-good. - Notifications with their own sound are played twice: the per-app
switch for that app is enabled (you chose to hear both sounds). Disable it
in the GUI to keep only the app's own sound.
If an app sends
suppress-sound, NotifySound is silent by design. - A notification arrives but plays no sound: if the app sends
suppress-soundthis is intentional (the app plays its own sound). If not, check the per-app toggle for that app in the GUI. - Sound plays only after the next notification: update to 0.1.1 or later; NotifySound processes each D-Bus message immediately. If the player starts immediately but audio is delayed, report it with details on your shell and notification server.
- Dependencies on Fedora: install them with
sudo dnf install python3-gobject gtk4 libcanberra-gtk3 dbus(GTK4 is only needed for the GUI). For MP3/M4A playback, also installsudo dnf install gstreamer1-plugins-base gstreamer1-plugins-good. - Dependencies on Arch: install them with
sudo pacman -S python-gobject gtk4 libcanberra dbus(GTK4 is only needed for the GUI), plussudo pacman -S gst-plugins-base gst-plugins-goodfor MP3/M4A playback. An official AUR package is planned but not available yet — install manually with./install.shfor now. canberra-gtk-playnot found: the package that provides it has a different name per distro — Debian:gnome-session-canberra, Fedora:libcanberra-gtk3, Arch:libcanberra. Find it withdnf provides canberra-gtk-play(Fedora) orpacman -Fy canberra-gtk-play(Arch).dbus-monitornot found: it is part of thedbuspackage, present on every desktop. If it is missing, install it withsudo dnf install dbus(Fedora) orsudo pacman -S dbus(Arch).
notify-sound # CLI entrypoint: --daemon | --quit | --gui (default)
notify_sound/
├── config.py # config/state/pidfile paths, autostart, JSON load/save
│ # (validates types, tolerates corrupt files, migrates
│ # the pre-0.1.1 "custom_sound" key), instance lock
├── daemon.py # dbus-monitor subprocesses, output parsers, play rules,
│ # backoff restarts, SIGTERM handling
├── player.py # playback: canberra for OGG/WAV/FLAC, fallback chain
│ # (gst-launch-1.0 → ffplay → mpv → mpg123) for the rest
├── sounds.py # discovers the active sound theme's available sounds
├── gui.py # GTK4 settings app (single instance)
└── __init__.py # __version__
tests/test_regression.py # unittest regression suite
share/notify-sound.service # systemd unit template
install.sh # per-user installer (PREFIX-aware)
build-deb.sh # Debian package builder
python3 -m unittest discover -s tests -vThe suite uses the notification() and gtk_notification() helpers to build
realistic dbus-monitor output (including multi-line bodies and hints) and
asserts on what would be played/skipped. Add a test for every new behavior.
- The reader thread consumes dbus-monitor's stdout line by line. A standard
Notifymessage starts at a line matching_MESSAGE_HEADER_REand ends at its top-levelint32 <n>line (_TOP_LEVEL_INT32_RE: exactly 3 spacesint32). This is the Notify timeout argument — always the last argument, and the only top-levelint32in a Notify message.
- GTK applications such as Ptyxis use
org.gtk.Notifications.AddNotification. Those messages end at the closing bracket of their outer top-levelarray, outside quoted strings. Incomplete or oversized messages are discarded. - Do not reintroduce blank-line or column-0 based message separation:
string values with embedded newlines (
"a\n\nb") print as physical blank and unindented lines inside a message. This was the root cause of double playback for chat apps (v0.1.4). - Framing markers and message headers are recognized only outside quoted strings, so notification content cannot imitate a terminator or header.
- gnome-shell re-sends every notification with
x-shell-sender+x-shell-sender-pidhints; those blocks must be skipped. - The canonical app name (used for the per-app config lookup and for the
auto-detected app list) is resolved, in order, from:
- the
desktop-entryhint when present and valid; otherwise - the process
commresolved from the D-Bussenderconnection viadbus-send … GetConnectionUnixProcessID+/proc/<pid>/comm(cached 5 min; skips generic interpreters likepython3/sh, fallings back to/proc/<pid>/cmdline); otherwise - the first matching synonym declared in another app's
synonymslist inconfig.json; otherwise - the raw first
stringargument (app_name). This fixes apps like AIMP that send the song title asapp_nameand would otherwise create one list entry per song. Thedesktop-entryhint value is read from thevariant string "..."line that follows thestring "desktop-entry"key; quoted strings stay tracked by the same state machine, so neither the key nor the value can fake framing.
- the
- Playback rules, in order: master
enabledoff → nothing;suppress-soundhint → always nothing; per-app disabled → nothing;sound-file/sound-namehint with no per-app config (the app is not inconfig.apps) → nothing (OWN-001: the app plays its own sound and the user did not choose to duplicate it); otherwise play the app's choice or the global choice (a theme id or a custom file path).
- Single instance:
config.acquire_instance_lock()takes anflockon the permanent lock file ($XDG_RUNTIME_DIR/notify-sound.pid.lock, per-user fallback in~/.cache/notify-sound/), opened withO_NOFOLLOWand mode 0600. The PID is written atomically to the adjacent.pidfile. dbus-monitoris restarted with exponential backoff (1s → 30s) if it exits; the backoff resets after 5s of stability. If the binary is missing the daemon retries in the background instead of dying. It observes bothorg.freedesktop.Notifications.Notifyandorg.gtk.Notifications.AddNotification.SIGTERM/SIGINTstop the main loop, terminate the monitor and remove the pidfile.--quitsendsSIGTERMto the pid in the pidfile.
-
Bump
__version__innotify_sound/__init__.py. -
Add a Changelog entry below and update versioned release links.
-
Run the full test suite.
-
./install.shto deploy, then restart the daemon (notify-sound --quit && notify-sound --daemon). -
Merge through a PR with the required
testscheck. -
Build the Debian package and its checksum:
./build-deb.sh sha256sum dist/notify-sound_X.Y.Z_all.deb \ > dist/notify-sound_X.Y.Z_all.deb.sha256 -
Create and push the matching
vX.Y.Ztag, create a draft GitHub release, upload the.deband checksum, then publish the release. -
Verify the release checksum and the successful GitHub Pages deployment.
- 0.1.10 — fix app-info counters and timestamps not updating after
the first notification (the daemon now persists
seen_count/last_seenon every_record_app); fix long app names being cut by widening the window and giving the name label a 28-char minimum, compacting the per-row Probar button into a play icon with tooltip. Fix process name truncation: fall back to/proc/PID/cmdlinewhencommis kernel-truncated to 15 chars (telegram-deskto→telegram-desktop). GUI improvements: custom sounds list is capped to ~3 rows with its own scroll (so it no longer steals vertical space from the apps list); app list can be sorted by arrival (default), by name, or by notification count via a new dropdown in the "Aplicaciones" header; the daemon status label moved next to the Start/Stop buttons; per-app remove button and "Vaciar lista" to reset the whole detected list. - 0.1.9 — auto-consolidate apps like AIMP that send the song title as
app_nameand nodesktop-entryhint: the daemon now resolves the D-Bussenderconnection to a PID (dbus-send org.freedesktop.DBus.GetConnectionUnixProcessID) and uses the processcomm/cmdlinename as a stable canonical id (cached 5 min, tolerant whendbus-sendor/procis unavailable, and skipping generic interpreters likepython3/sh). The canonical chain is now:desktop-entry→ resolvedcomm→ known synonym → rawapp_name. Manual merge: renaming an app to an alias already in use opens a dialog to fuse the two entries (the duplicate becomes a synonym, withenabledAND-combined andsoundinherited); duplicate aliases left inconfig.jsonare also merged automatically on load. New "Información" popover (per app) shows the raw notification name, the display alias, the detected process, synonyms, notification count and last-seen time.state.jsonnow storesapp_meta(count /last_seen/comm) retrocompatibly.MAX_SYNONYMS = 64bounds the synonym lists. - 0.1.8 — fix AIMP and other apps that send the song title as the
notification
app_name: when thedesktop-entryhint is present, it is used as the canonical app name (so the GUI shows one stable "aimp" row instead of one entry per song); manual app aliasing via the new "Renombrar" button (Popover withGtk.Entry) for cases where the hint is not sent; GUI fixes: long app names now ellipsize with a tooltip instead of pushing the dropdown/button/switch out of view, and every switch is vertically centered (valign=CENTER) so it no longer stretches with the row height. Parser regression tests added fordesktop-entry,x-shell-sender+desktop-entry,sound-name+desktop-entryand per-app config lookup by canonical name. - 0.1.7 — Debian package and checksum improvements, FLAC theme sounds, and a complete release checklist.
- 0.1.6 — security hardening: quote-aware bounded notification parsing, GTK notification support (including Ptyxis), private atomic config/state writes, bounded playback and decoder timeouts, safe sound/path validation, hardened installer/autostart, and pinned Pages actions.
- 0.1.5 — fix daemon shutdown: terminate/wait/kill ordering so
--quitalways stops the daemon and itsdbus-monitorcleanly. - 0.1.4 — fix multiline parsing: message end detected via the top-level
int32timeout line instead of blank lines (multiline bodies caused double playback and hid hints). Respect thesuppress-soundhint always (apps that manage their own sound stay silent — no more double sound with Chromium PWAs like WhatsApp Web). - 0.1.3 — GUI releases its hold on window close, so the process exits instead of staying resident.
- 0.1.2 — GUI is single-instance: re-launching focuses the existing window instead of opening a new one.
- 0.1.1 — harden daemon and config: immediate parsing (no delay until the
next notification), safe playback when
canberra-gtk-playis missing, exponential restart backoff, atomic instance lock (no stale pidfiles, no/tmpcollision), tolerant config parsing, PREFIX-aware install/autostart, playback fallback for MP3/other formats, regression suite. - 0.1.0 — initial release: daemon, GTK4 GUI, theme/custom sounds, per-app config, autostart, install script.
Background: GNOME plays notification sounds only when the notification carries a sound hint (per the freedesktop notification spec). Relevant discussions:
- gnome-shell maintainer explaining the behaviour
- Warp: play_notification_sound is macOS-only / feature request: sound after long commands
MIT