Sound System Architecture β
UI audio coordination for LCARdS β
window.lcards.core.soundManager
The Sound System provides event-driven audio feedback for all LCARdS interactions and HA UI events. It is a single-instance BaseService that lives at window.lcards.core.soundManager.
ποΈ System Overview β
Architecture Components β
SoundManager (BaseService singleton)
β
ββ Tier 1 β Card interactions
β ββ LCARdSActionHandler.setupActions()
β calls soundManager.play(eventType, { cardOverride })
β
ββ Tier 2 β Global HA UI listeners (document-level)
β ββ click (capture) β nav_sidebar, menu_expand
β ββ location-changed β nav_page
β ββ hass-action β card_tap / card_hold / card_double_tap
β ββ show-dialog β dialog_open
β ββ hass-more-info β more_info_open
β ββ dialog-closed β dialog_close
β ββ history.replaceState patch β dashboard_edit_start / dashboard_edit_save
β
ββ Scheme Registry (Map<name, eventMap>)
β ββ Registered via PackManager.registerPack() β registerSchemes()
β
ββ Audio Cache (Map<assetKey, HTMLAudioElement>)
β ββ URL resolved from AssetManager registry (no async preload needed)
β
ββ Override Store
ββ localStorage key: 'lcards_sound_overrides' (JSON eventTypeβassetKey map)Key Files β
| File | Purpose |
|---|---|
src/core/sound/SoundManager.js | Core singleton β all playback, listeners, scheme management |
src/core/packs/sounds/builtin-sounds.js | lcards_default scheme + asset definitions |
src/core/helpers/lcards-helper-registry.js | Helper definitions for 6 sound config helpers |
π Event Types & Categories β
Event types are grouped into three categories, each controlled by an independent input_boolean helper.
cards β input_boolean.lcards_sound_cards β
| Event | Trigger |
|---|---|
card_tap | Any tap on a LCARdS card or standard HA card |
card_hold | Hold action |
card_double_tap | Double-tap action |
card_hover | Hover (desktop pointer) |
button_tap | LCARdS button element tap |
toggle_on / toggle_off | Toggle state change |
slider_drag_start / slider_drag_end | Slider grab / release |
slider_change | Per-tick slider value change (silenced in default scheme) |
more_info_open | More-info panel opened |
ui β input_boolean.lcards_sound_ui β
| Event | Trigger |
|---|---|
nav_sidebar | Sidebar navigation item click |
menu_expand | Hamburger / expand icon click |
nav_page | location-changed event (view/page navigation) |
dialog_open | Any HA dialog opens (except more-info) |
dialog_close | Any HA dialog dismissed (save or cancel) |
dashboard_edit_start | Dashboard edit mode entered |
dashboard_edit_save | Dashboard edit mode exited |
alerts β input_boolean.lcards_sound_alerts β
| Event | Trigger |
|---|---|
alert_activate | Alert mode set to red / yellow / blue |
alert_clear | Alert mode set to green (cleared) |
alert_escalate | Alert escalation (reserved) |
system_ready | LCARdS initialization complete |
error | System error condition |
notification | General notification event |
βοΈ HA Helper Integration β
Six input helpers store sound configuration. They are created via the Sound Config Panel or manually via YAML.
| Helper key | Entity ID | Type | Purpose |
|---|---|---|---|
sound_enabled | input_boolean.lcards_sound_enabled | boolean | Master on/off (default off β opt-in) |
sound_cards_enabled | input_boolean.lcards_sound_cards | boolean | Card interaction category |
sound_ui_enabled | input_boolean.lcards_sound_ui | boolean | UI navigation category |
sound_alerts_enabled | input_boolean.lcards_sound_alerts | boolean | Alerts category |
sound_volume | input_number.lcards_sound_volume | number 0β1 | Master volume |
sound_scheme | input_select.lcards_sound_scheme | select | Active scheme name |
Category opt-out model: If a category helper doesn't exist yet, _isCategoryEnabled() returns true (play sounds). The master sound_enabled helper defaults to false (opt-in β no sounds until explicitly turned on).
Scheme options sync: When packs register schemes via registerSchemes(), SoundManager calls helperManager.updateSelectOptions('sound_scheme', schemeNames) to keep the input_select options in sync with loaded packs. This is non-fatal if the helper doesn't exist yet.
π΅ Sound Resolution Order β
play(eventType, context) resolves the asset key in strict priority order:
1. context.cardOverride
β null β explicitly silenced (return immediately)
β string β use this asset key directly
β
2. localStorage override (lcards_sound_overrides)
β { eventType: assetKey } β per-event user override
β
3. Active scheme mapping
β scheme[eventType] === null β scheme-silenced (return)
β scheme[eventType] === string β use this asset key
β
4. No mapping found β silence (return)π Global Listener Architecture β
mountGlobalUIListener() attaches all Tier 2 listeners. It is idempotent (safe to call multiple times; exits early if already mounted). Called by LCARdSCore after initialization.
Browser Autoplay Guard β
A one-shot capture-phase click listener sets _userInteracted = true. Until this fires, all _playAsset() calls are silently dropped (browser autoplay policy). preview() bypasses this guard via force=true.
Click Handler (sidebar sounds) β
Capture-phase click on document. Checks composedPath() for HA-SIDEBAR or role="navigation", then discriminates between hamburger buttons (HA-ICON-BUTTON) β menu_expand and nav items (PAPER-ICON-ITEM, role=option/menuitem) β nav_sidebar.
hass-action Handler β
Catches tap/hold/double_tap on non-LCARdS HA cards (Mushroom, built-in HA cards, etc.) that fire the composed hass-action event. Guards against double-firing by skipping events whose composedPath() passes through any LCARDS-* element (LCARdS cards handle their own sounds via LCARdSActionHandler).
show-dialog Handler β
Fires on any HA dialog open except ha-more-info-dialog (which is handled separately as more_info_open to give it a distinct event type).
dialog-closed Handler β
Fires on any HA dialog dismiss (save, cancel, ESC, or close button). Skips dialogs whose localName starts with lcards- to avoid double-sounds from LCARdS-owned panels.
Dashboard Edit Mode (history.replaceState patch) β
HA uses history.replaceState (not a DOM event) to toggle ?edit=1 in the URL when entering/exiting dashboard edit mode. SoundManager patches window.history.replaceState, compares the edit=1 param before/after, and fires dashboard_edit_start or dashboard_edit_save on change. The original function is saved as _historyReplaceStateOrig and restored in destroy().
π€ Audio Asset Resolution β
Asset URLs are resolved synchronously from AssetManager's internal registry:
const registry = this._core?.assetManager?.getRegistry('audio');
const entry = registry?.assets?.get(assetKey);
const url = entry?.url;Audio elements are created once per asset key and cached in _audioCache. On each play:
audio.volumeis set from_getVolume()audio.currentTime = 0(allows rapid re-trigger)audio.play()β.catch(() => {})suppressesAbortErrorfrom fast replays
No async preloading is required β the browser fetches the URL on first play.
π¦ Pack Integration β
Sound packs declare sound_schemes and audio_assets in their pack definition:
export const MY_SOUND_PACK = {
id: 'my_sound_pack',
version: '1.0.0',
name: 'My Sound Pack',
// Registered with AssetManager
audio_assets: {
my_tap: {
url: '/hacsfiles/lcards/sounds/my_pack/tap.mp3',
description: 'Tap beep',
},
},
// Registered with SoundManager
sound_schemes: {
my_scheme: {
card_tap: 'my_tap',
nav_sidebar: 'my_tap',
card_hover: null, // Silence this event in this scheme
// Omitted events β silence
},
},
};PackManager.registerPack() routes audio_assets to AssetManager and sound_schemes to SoundManager.registerSchemes(). The sound_scheme input_select options are updated automatically after registration.
ποΈ Override Storage β
Per-event overrides are stored in localStorage under the key lcards_sound_overrides as a JSON object:
{ "card_tap": "my_tap", "nav_sidebar": null }null values are not stored β setOverride(eventType, null) deletes the key. _getOverrides() returns {} on parse failure (never throws).
β Adding a New Event Type β
- Add the key to
EVENT_CATEGORYinSoundManager.jswith the appropriate category ('cards','ui', or'alerts'). - Add a human-readable label to
SOUND_EVENT_LABELS. - Add a mapping to
LCARDS_DEFAULT_SCHEMEinbuiltin-sounds.js(usenullto silence by default). - Fire it via
soundManager.play('my_new_event')at the appropriate call site.
π Public API β
const sm = window.lcards.core.soundManager;
// Playback
sm.play('card_tap');
sm.play('card_tap', { cardOverride: 'my_asset' }); // card-level override
sm.play('card_tap', { cardOverride: null }); // silence this event
sm.preview('my_asset'); // bypass enable checks
sm.previewScheme('lcars_classic', 'card_tap'); // preview a scheme
// Overrides
const overrides = sm.getOverrides(); // { eventType: assetKey }
await sm.setOverride('card_tap', 'my_asset'); // set override
await sm.setOverride('card_tap', null); // clear override
await sm.clearAllOverrides(); // clear all
// Scheme introspection
const names = sm.getSchemeNames(); // ['none', 'lcards_default', ...]
const events = sm.getEventTypes(); // [{ key, label, category }, ...]
// Lifecycle
sm.mountGlobalUIListener(); // called by LCARdSCore
sm.subscribeToAlertMode(); // called by LCARdSCore
sm.destroy(); // cleanup