---
title: Smart Background Sound | YOOtheme Pro Audio Plugin
description: Add optional (ambient) sound to YOOtheme Pro with accessible controls, smart media detection, no autoplay surprises and browser-friendly audio.
url: https://www.yoozecase.com/smart-background-sound
---

# Smart Background Sound

Smart Background Sound

**Latest Version**

0.9.18

**Release date**

04 August 2026

**Made for**

Yootheme Pro, Joomla

**Price**

Free

[🛒 Get it for Joomla 👍](https://shop.yoozecase.com/checkout/buy/e7db9e86-781b-41ff-a8ea-9bcfc682390b)

[🛒 Get it for WordPress👍](https://shop.yoozecase.com/checkout/buy/7e38c651-f07c-4352-bbb1-ca9cfc8968d3)

[Demo page](https://www.yoozecase.com/smart-background-sound-demo)

Tasteful ambient sound with a smart mute. Used responsibly, we promise.

![Smart Background Sound](https://www.yoozecase.com/files/smart-background-sound.png)

---

## Description

---

#### Top voted

';
 }
 html += '

';
 if (oc.title) {
 html += '

##### ' + escapeHtml(oc.title) + '

';
 }
 if (oc.text) {
 html += '

' + escapeHtml(oc.text).replace(/\n/g, '
') + '

';
 }
 html += '

';
 }

 // Auto winner(s) - top N from the already vote-sorted features
 if (mode === 'auto_winner' || mode === 'both') {
 var n = oc.winnerCount | 0;
 if (n ';
 for (var i = 0; i ' +
 (w.image_url ? '

![](https://www.yoozecase.com/' + escapeHtml(w.image_url) + ')

' : '') +
 '' + (i + 1) + '' +
 '' + escapeHtml(w.title) + '' +
 '' + getIcon(this.cfg.voteIcon) + ' ' + (w.votes | 0) + '' +
 '';
 }
 html += '';
 }
 }

 if (html === '') {
 this.elOutcome.hidden = true;
 return;
 }
 this.elOutcome.innerHTML = html;
 this.elOutcome.hidden = false;
 };

 /**
 * Roadmap view (v3.2.0): renders the items as status columns, in the
 * canonical status order from cfg.statuses. Each column is an  inside
 * the existing .yc-fb-list ';
 }
 return html;
 };

 Board.prototype.renderItem = function (f, index) {
 var disabled = this.cfg.isClosed ? 'disabled' : '';
 var voted = f.voted ? ' yc-voted' : '';
 var descHtml = f.description
 ? '

' + escapeHtml(f.description).replace(/\n/g, '
') + '

'
 : '';
 var thumbHtml = f.image_url
 ? '

![](https://www.yoozecase.com/' + escapeHtml(f.image_url) + ')

'
 : '';

 // Status workflow (v3.0.0): badge for any non-default status. The
 // roadmap view (v3.2.0) skips the badge - the column already
 // communicates the status, a badge would be redundant noise.
 var st = (f.status || 'open');
 var labels = this.cfg.statusLabels || {};
 var statusBadge = (!this.isRoadmap && st && st !== 'open' && labels[st])
 ? '' + escapeHtml(labels[st]) + ''
 : '';

 // Design themes (v3.1.0): arena / neon / pixel render extra per-item
 // furniture - rank number, avatar (image or initials), a "leader" pill
 // on rank 1, and a "votes" label. Classic/colour themes are untouched.
 var t = this.cfg.theme || 'light';
 var themed = (t === 'arena' || t === 'neon' || t === 'pixel');
 var rank = (typeof index === 'number') ? (index + 1) : 0;
 var rankHtml = '', leaderHtml = '', avatarHtml = '', votesLabel = '';
 if (themed) {
 rankHtml = '' + pad2(rank) + '';
 if (rank === 1) {
 leaderHtml = '' + escapeHtml(this.cfg.leaderLabel || 'Leader') + '';
 }
 if (f.image_url) {
 avatarHtml = '

![](https://www.yoozecase.com/' + escapeHtml(f.image_url) + ')

';
 } else {
 avatarHtml = '

' + escapeHtml(initialsOf(f.title)) + '

';
 }
 votesLabel = '' + escapeHtml(this.cfg.votesLabel || 'votes') + '';
 }

 // Moderation overlay: subtle corner buttons, only when canModerate.
 // Buttons appear on hover/focus (styled in CSS). Even in customizer
 // preview we render them when canModerate is true so the moderator
 // can see what visitors with moderation rights will see.
 var modHtml = '';
 if (this.cfg.canModerate) {
 var parts = [];
 var hideTitle = escapeHtml(this.t('hideTitle', 'Hide this feature'));
 var deleteTitle = escapeHtml(this.t('deleteTitle', 'Delete this feature'));
 if (this.cfg.allowSoftHide) {
 parts.push(
 ''
 );
 }
 if (this.cfg.allowHardDelete) {
 parts.push(
 ''
 );
 }
 if (parts.length || (this.cfg.statuses && this.cfg.statuses.length)) {
 var sParts = parts.slice();
 if (this.cfg.statuses && this.cfg.statuses.length) {
 var opts = '';
 for (var si = 0; si ' + escapeHtml(sl) + '';
 }
 var setStatus = escapeHtml(this.t('setStatus', 'Set status'));
 sParts.push(
 ''
 );
 }
 modHtml = '';
 }
 }

 var mediaHtml = themed ? avatarHtml : thumbHtml;
 return (
 '' +
 leaderHtml +
 rankHtml +
 '' +
 mediaHtml +
 '

' +
 '

##### ' + escapeHtml(f.title) + '

' +
 statusBadge +
 descHtml +
 '

' +
 modHtml +
 ''
 );
 };

 /**
 * Detects whether a JSON response looks like a CSRF/token rejection.
 * Two cases:
 * 1. com_ajax-level rejection (Joomla's auto-token-check on POST): top
 * level message contains the JINVALID_TOKEN_NOTICE text and HTTP
 * status is typically 403.
 * 2. Plugin-level rejection from requireToken(): payload.error === "Invalid token".
 */
 Board.prototype.isTokenError = function (status, raw, payload) {
 if (status === 403) return true;
 var hay = '';
 try { hay = JSON.stringify(raw || {}) + '|' + JSON.stringify(payload || {}); } catch (e) {}
 return /invalid (security )?token/i.test(hay);
 };

 /**
 * Fetch a fresh CSRF token from the server. Updates this.cfg.csrfToken so
 * the next call (and any future calls in this Board's lifetime) use the
 * correct one. Cache-bust with a timestamp so no upstream CDN caches the
 * response.
 */
 Board.prototype.fetchFreshToken = function () {
 var self = this;
 var url = this.cfg.ajaxUrl + '&task=token&_=' + Date.now();
 return fetch(url, {
 method: 'GET',
 credentials: 'same-origin',
 headers: { 'Accept': 'application/json', 'Cache-Control': 'no-cache' }
 })
 .then(function (r) { return r.json(); })
 .then(function (data) {
 var p = self.unwrapAjax(data);
 if (p && p.ok === true && typeof p.token === 'string' && p.token.length >= 16) {
 self.cfg.csrfToken = p.token;
 return p.token;
 }
 throw new Error('No fresh token in response');
 });
 };

 /**
 * Generic POST-with-token helper. Builds FormData from a plain object,
 * adds the current token, posts. On 403 / token-error response, fetches a
 * fresh token and retries exactly once.
 *
 * @param {string} task The task name (appended to URL and body)
 * @param {object} fields Key-value form fields (no token)
 * @return {Promise

## Changelog

---

v0.9.18
 04/08/2026

Changed

- Licensing moved to GPL-2.0-or-later, in line with the YOOzeCase licensing decision of 2 August 2026. The manifest now declares `GNU General Public License version 2 or later`, `LICENSE.txt` carries the full GPL text, and `COPYRIGHT.txt` and `NOTICE.txt` no longer describe the software as proprietary. Every PHP, JS and CSS file carries the GPL header instead of the previous restricted-access notice.
- This changelog is now in English throughout. Roughly 90 entries from 0.2.5 up to 0.9.15 were written in Dutch while the product pages, the README and the plugin itself are English. Code identifiers, file names, language keys and version numbers are unchanged.
- Release tooling refreshed to the current YOOzeCase standard: `scripts/pre-release-grep-check.sh`, `scripts/selftest-grep-check.sh` and `scripts/check-php-matrix.sh` now check for the GPL header rather than requiring the old notice.
- No functional change. Nothing in the runtime behaviour, the element configuration or the stored settings is affected by this release.
- What you buy remains a subscription to downloads, updates and support. The GPL covers the source code; it does not cover the YOOzeCase name, branding, product images or documentation.

Removed

- `TERMS.txt` is no longer part of the package. The commercial terms live at https://www.yoozecase.com/terms; the licence that travels with the code is the GPL.

v0.9.17
 02/07/2026

Fixed

- Degraded state is now final: when an audio source fails, late

v0.9.16
 02/07/2026

Added

- Audio error handling: when the configured audio source fails to load
- Mixed-content guard: an http:// audio URL on an https site is upgraded
- Update-server feeds: updates.xml and changelog.xml are now delivered

Fixed

- The plugin config switch "Enabled" now actually disables the plugin:

Security

- validateAudioUrl strips control characters (0x00-0x1F, 0x7F) before
- AI warning header harmonised to the canonical text (including the

v0.9.15
 22/06/2026

Added

- External audio URL as a validated source: besides a local file, the "Audio file" field now explicitly accepts a direct external audio URL (CDN or your own host). Client-side `<audio>`, no server fetch (no SSRF). New README section "External audio URL" covering the points to watch (HTTPS/mixed content, hotlinking, Content-Type, no YouTube/Spotify because of their terms of service).

Changed

- `resolveAudioUrl` in template.php now validates every URL against a scheme whitelist: `http(s)://`, protocol-relative (`//`), root-relative (`/`) and Joomla routes (`index.php?...`) are allowed; dangerous or non-audio schemes (`javascript:`, `data:`, `vbscript:`, `file:`) are rejected, after which the element falls back cleanly to the degraded button instead of emitting a malformed `src`. Previously every string was passed through unchecked.

v0.9.14
 22/06/2026

Fixed

- Visibility pause did not really mute a background tab: since the fade landed (v0.9.6), muting ran through `requestAnimationFrame`, which browsers freeze in a hidden tab, so `audio.muted = true` (set only in the fade completion) was never reached and the music kept playing at full volume for as long as the tab sat in the background. The tab-hidden branch now sets `audio.muted = true` synchronously, independently of the fade (functional audit, item 1).
- Variant 1 (always on) did not show the activation coachmark when the browser blocked autoplay, while variants 2/3/4 do. Variant 1 now has the same 800ms fallback that still shows the coachmark if the button drops to the off state (functional audit, item 2).

v0.9.13
 19/06/2026

Added

- Activation coachmark: a one-time, self-dismissing hint next to the button on a first visit (in the off state only), inviting the visitor to start the music. The text comes from the language layer (`_COACHMARK`, 6 languages) and is remembered site-wide in localStorage (`yc_sbs_coach_seen`) so it does not repeat on every page. New element field "First-visit hint" (Behaviour tab, on by default). Raises activation for the off-by-default variants (improvement audit, item 1).
- Optional event hook: `yc-music:play` and `yc-music:pause` are dispatched as DOM CustomEvents on the element so the site owner can wire up GA4/Matomo themselves. The plugin sends no data of its own. New element field "Emit JS events" (Behaviour tab, off by default) - privacy-clean and provider-independent (improvement audit, item 3).

Changed

- Play/pause affordance: the music note icon morphs into two pause bars via CSS while playing, making it clearer that a second click mutes. Respects `prefers-reduced-motion` (improvement audit, item 1).
- Follow-up to the improvement audit of v0.9.12 (audit_plg_system_yc_smart_background_sound_v0.9.12.md). No security impact; the event hook is opt-in and sends no data to third parties. Language key parity 99 keys x 6 languages.

Fixed

- End state with loop=off: when a non-repeating track finishes naturally, the plugin now returns the button to the off state (icon, `aria-pressed`, label), so a single click starts it again. Previously the button stayed visually stuck in the playing state (improvement audit, item 5).

v0.9.12
 17/06/2026

Changed

- Preflight PHP version message (`script.php`): the hardcoded Dutch text has been replaced by a neutral English literal with an explanation. Deliberately no language layer: preflight runs before the package files (including the language files) are copied, so a Text key would render raw at that point (software audit, item 2; edge case, Joomla 5 already requires PHP 8.1+).
- `update()` in `script.php`: `@unlink()` replaced by `Joomla\Filesystem\File::delete()` inside a try/catch, consistent with `cleanupLegacyFiles()`.
- Version sync to 0.9.12 via `tools/bump-version.sh`.
- Follow-up to the software audit of v0.9.11 (audit_plg_system_yc_smart_background_sound_v0.9.11.md). No security impact; no functional change for visitors.

Removed

- Diagnostic DOM attributes `data-yc-music-raw-type` and `data-yc-music-raw-value` from `template.php` (plus the `$debugType`/`$debugRaw` build-up). They were emitted on every render but never read by the JS (dead debug output) and leaked the internal audio field structure into the public DOM (software audit, item 1; skill 10.4/6.3).
- Unused `public $app` property from the main plugin class.

v0.9.11
 14/06/2026

Added

- `element.php`: defensive language load (`$lang->load(... JPATH_ADMINISTRATOR)` with a fallback to the plugin path) as a safety net, so the Text keys still resolve if YOOtheme loads the element before the plugin has autoloaded its language.

Changed

- Builder UI fully routed through the Joomla language layer (gate item 8). All field labels, descriptions, choice options and tab titles in `element.php` now go through `Text::_()` with keys in the six base languages, instead of hardcoded Dutch. The builder config therefore follows the admin language. 78 new keys per language (`..._B_*`), parity 94 keys x 6 languages.
- README fully revised: current `--yc-music-*` token names (the README still documented the `--music-*` names that were renamed in v0.9.8), a complete configuration reference per tab, duck detection including the recognised hosts and the auto-resume behaviour, a browser compatibility table, and a Troubleshooting section (sticky `yc_sbs_paused_*`, autoplay blocking, a WAF that refuses uploads). Installation no longer points at a fixed old zip version number.
- SECURITY.txt, NOTICE.txt and TERMS.txt reviewed; the content remains current and compliant (no change needed).
- Version sync to 0.9.11 across all locations via `tools/bump-version.sh`.

v0.9.10
 14/06/2026

Added

- Tooling baseline (YC.15, phase 3 of the v1.0.0 track). Dev tooling, NOT installed by Joomla (not in the manifest `<files>`); it lives in the repo/zip for pre-release checks:
- `scripts/pre-release-grep-check.sh` (canonical): grep loop, security scan and YC gate checks
- `scripts/selftest-grep-check.sh` (canonical): self-test that asserts the main script keeps firing (28 assertions green)
- `scripts/check-php-matrix.sh`: Docker `php -l` across PHP 8.1/8.2/8.3/8.4, with paths adapted to this plugin (`modules/` plus root, no `src/services`)
- `.phpcs.xml.dist`: PHP_CodeSniffer with the Joomla coding standard (hidden `.dist` form so the manifest detection of the grep check is not shadowed)
- `phpstan.neon` (level 6) plus `phpstan-baseline.neon` (the baseline is generated on the dev server, where the Joomla framework autoloader is active)
- `eslint.config.js`: ESLint 9 flat config, tuned to the deliberately vanilla ES5-style JS; lint runs green (0 errors, 0 warnings)
- `composer.json` plus `package.json` with the dev dependencies for reproducible tooling

Changed

- Version sync to 0.9.10 across all locations (manifest plus creationDate, script.php `PLUGIN_VERSION`, JS and CSS header, template `assetVersion`, composer.json, package.json) via `tools/bump-version.sh`

v0.9.9
 12/06/2026

Added

- Central CSS design tokens (sizes, spacing, radius, light/dark colours, shadows) at the top of the stylesheet; component variables consume the tokens with a fallback (YC 7A). The tokens sit on `.yc-music` AND `.yc-music-toggle` because in flavours 3/4 the button is mounted outside the element
- Responsive breakpoints 1024/640/420: label widths capped, smaller button offset on smartphones, on a narrow screen the tooltip flips below the button for top positions, no overflow at 320px (label max width bounded by the viewport)
- Tap target of at least 44x44px on touch devices via `pointer: coarse` (the visual design on desktop is unchanged) (YC 7F)
- Language keys ARIA_DEGRADED and TITLE_DEGRADED in all six languages; the degraded button now follows the site language instead of hardcoded Dutch

Changed

- Dark theme background from 0.55 to 0.65 opacity (hover 0.82): label text now reaches 4.5:1 contrast on fully light backgrounds (WCAG 2.1 AA)

Fixed

- Language load in postflight now uses `$reload = true`, with the legacy cleanup BEFORE the load: during an update the old plugin version has already loaded the language file, which made the load a cached no-op and caused new keys to render raw (lesson learned from the v0.9.8 installation)
- bump-version.sh: the assetVersion line was skipped because sed escaped the dollar sign
