Skip to content

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 ​

FilePurpose
src/core/sound/SoundManager.jsCore singleton β€” all playback, listeners, scheme management
src/core/packs/sounds/builtin-sounds.jslcards_default scheme + asset definitions
src/core/helpers/lcards-helper-registry.jsHelper 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 ​

EventTrigger
card_tapAny tap on a LCARdS card or standard HA card
card_holdHold action
card_double_tapDouble-tap action
card_hoverHover (desktop pointer)
button_tapLCARdS button element tap
toggle_on / toggle_offToggle state change
slider_drag_start / slider_drag_endSlider grab / release
slider_changePer-tick slider value change (silenced in default scheme)
more_info_openMore-info panel opened

ui β€” input_boolean.lcards_sound_ui ​

EventTrigger
nav_sidebarSidebar navigation item click
menu_expandHamburger / expand icon click
nav_pagelocation-changed event (view/page navigation)
dialog_openAny HA dialog opens (except more-info)
dialog_closeAny HA dialog dismissed (save or cancel)
dashboard_edit_startDashboard edit mode entered
dashboard_edit_saveDashboard edit mode exited

alerts β€” input_boolean.lcards_sound_alerts ​

EventTrigger
alert_activateAlert mode set to red / yellow / blue
alert_clearAlert mode set to green (cleared)
alert_escalateAlert escalation (reserved)
system_readyLCARdS initialization complete
errorSystem error condition
notificationGeneral notification event

βš™οΈ HA Helper Integration ​

Six input helpers store sound configuration. They are created via the Sound Config Panel or manually via YAML.

Helper keyEntity IDTypePurpose
sound_enabledinput_boolean.lcards_sound_enabledbooleanMaster on/off (default off β€” opt-in)
sound_cards_enabledinput_boolean.lcards_sound_cardsbooleanCard interaction category
sound_ui_enabledinput_boolean.lcards_sound_uibooleanUI navigation category
sound_alerts_enabledinput_boolean.lcards_sound_alertsbooleanAlerts category
sound_volumeinput_number.lcards_sound_volumenumber 0–1Master volume
sound_schemeinput_select.lcards_sound_schemeselectActive 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:

javascript
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:

  1. audio.volume is set from _getVolume()
  2. audio.currentTime = 0 (allows rapid re-trigger)
  3. audio.play() β€” .catch(() => {}) suppresses AbortError from 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:

javascript
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:

json
{ "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 ​

  1. Add the key to EVENT_CATEGORY in SoundManager.js with the appropriate category ('cards', 'ui', or 'alerts').
  2. Add a human-readable label to SOUND_EVENT_LABELS.
  3. Add a mapping to LCARDS_DEFAULT_SCHEME in builtin-sounds.js (use null to silence by default).
  4. Fire it via soundManager.play('my_new_event') at the appropriate call site.

πŸ”Œ Public API ​

javascript
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