{
  "schema": "leumas.docs.page/1",
  "id": "pkg:@leumas/sound",
  "slug": "packages/sound",
  "kind": "capabilities",
  "bucket": "package",
  "title": "@leumas/sound",
  "name": "@leumas/sound",
  "eyebrow": null,
  "chip": null,
  "summary": "The Leumas UI sound layer — a synthesised sound bank, a non-spatial mixer and ONE delegated DOM listener set, so every button, tab, toggle and window in the ecosystem sounds without a single...",
  "keywords": [
    "[sound, sfx, audio, ui feedback, click, hover, web audio, synth, mixer, mute]"
  ],
  "audience": "both",
  "funnel": {
    "product": null,
    "cta": null
  },
  "body": "\n# @leumas/sound\n\nThe faint layer under the interface. A button you press answers you, a window that opens says so, and\na quest that completes rings — at about a seventh of full scale, on a bank of sounds that ships as\n**zero bytes**.\n\nTwo interface packs over one 24-cue vocabulary: **`os`**, the macOS register (short, soft, warm),\nand **`hud`**, the game register (brighter, more travel in the bends). A surface asks for `click`; the\npack decides what a click sounds like there.\n\nAnd a third, **`world`** — 27 voices for the 3D world, on a vocabulary of its own that shares not one\ncue with the other two. Footsteps, engines, water, weather, contact. It plays through the world's own\nspatial mixer, never through the interface one.\n\n## Why there are no audio files\n\nEvery voice is rendered from a parameter table into an `AudioBuffer` on the first user gesture — a\nband-limited noise burst plus one or two sines, each under its own exponential decay, under a shared\nraised-cosine attack. For UI transients that is not a compromise:\n\n- **Quality.** A macOS tick *is* a soft-attack filtered click with a quiet inharmonic partial. A\n  recording gets no closer for this material.\n- **Consistency.** One tuning table means the two packs are provably the same family.\n- **Performance.** No network, no decode, nothing on any product's bundle. The whole bank is a couple\n  of milliseconds of arithmetic, once.\n\nThe model is `@leumas/game-3d`'s `synth.js`, generalised — that file now imports `renderVoice` from\nhere and keeps only its own table. Its header makes the same argument for gunshots.\n\n## Layout\n\n| Path | Owns |\n|---|---|\n| `src/engine/synth.js` | `renderVoice` + `renderPack` — the sample renderer, and the four fields a UI tick needs that a gunshot does not |\n| `src/engine/packs.js` | **the sound design**: `OS_PACK`, `HUD_PACK`, `WORLD_PACK`. Loaded LAZILY — see `bank.js` |\n| `src/engine/cues.js` | both vocabularies, their categories, and `SUSTAINED` — the synchronous half `play()` gates on |\n| `src/engine/bank.js` | the one lazy boundary: the renderer and the tables, off every boot path |\n| `src/engine/mixer.js` | the pool, the latch, and the rules that keep this pleasant rather than irritating |\n| `src/engine/backend.js` | `createWebAudioBackend` (one context per page) · `createSilentBackend` (for tests) |\n| `src/install.js` | `installSfx()` — ONE delegated listener set for the whole document |\n| `src/prefs.js` | the local mirror and the flat `sound.*` profile keys |\n| `src/react/SoundProvider.jsx` | `SoundProvider` · `useSound` · `useSoundPack` · `SfxHost` |\n| `src/react/SoundSettingsCard.jsx` | the audition card — every voice, a play button, live sliders |\n| `src/sound.css` | the card's paint, tokens only |\n\n## Entries\n\n| Subpath | For |\n|---|---|\n| `.` | the headless engine — no React, no DOM, safe on a guarded bundle path |\n| `./react` | `SoundProvider` · `SoundBoundary` · `useSound` · `useSoundPack` · `SfxHost` |\n| `./settings` | the audition card. Lazy-load it; it renders 50 voices |\n| `./install` | `installSfx()` and `emitSfx()`, for a surface with no React |\n| `./prefs` | `readLocalSound()` for a `main.jsx` that needs an answer before any provider exists |\n| `./sound.css` | imported by `./settings` itself; no product needs to import it |\n\n## Rules\n\n- **The engine half imports nothing but `@leumas/game-kit/rng`.** That is a subpath onto a file with\n  no imports at all; the game-kit *barrel* reaches rapier through `./physics` and must never appear\n  here. `check:deck` walks every static and dynamic edge from the Operator Deck and fails on new\n  weight, and this package sits on that path.\n- **No React under `src/engine/`.** It is imported by a Node test and by the game.\n- **Adding an interface cue means adding it to BOTH interface packs.** `test/mixer.test.js` asserts\n  it, because a cue present in one and missing from the other is a sound that vanishes when a person\n  switches packs, with nothing to attribute it to.\n- **The `world` vocabulary is separate and does NOT fall back.** `voiceFor('world', 'click')` is\n  `null` on purpose: the two sets share no cue, so a fallback would answer a mistyped world cue with\n  whatever interface voice happened to share its name. `world` is also **not offered in the settings\n  picker** (`UI_PACK_IDS`) — choosing it as an interface register would silence every button in the\n  app while looking perfectly configured.\n- **A looping voice carries `sustain: true` AND is listed in `SUSTAINED`.** The renderer overshoots\n  and folds the overrun back over the head so the buffer joins to itself; playing a one-shot on\n  `loop: true`, or a sustained voice as a one-shot, is a click at a perfectly regular interval.\n  `check:world-audio` measures the seam against each voice's own RMS.\n- **Sounds are added by data attribute, not by editing components.** `data-sfx=\"<cue>\"` overrides the\n  cue an element plays; `data-sfx=\"off\"` silences it and everything inside it; `data-sfx-hover` on a\n  container makes everything pressable inside it hover too. All three are found with `closest()`.\n  Reach for an explicit `useSound().play()` only where there is no DOM event at all — a route change,\n  a toast, a window opening — or `emitSfx()` where there is no React either.\n- **A hover must be dwelt on for 60ms.** Sweeping a pointer across a grid of icons on the way\n  somewhere else touches a dozen of them in a fifth of a second; without the dwell that is a dozen\n  sounds for a gesture that was about none of them, and the mixer's 120ms rate limit only thins it to\n  a machine gun at a lower rate. `test/install.test.js` pins the sweep silent.\n- **Never call `play` on scroll, on pointer movement or on a keystroke.** No rate limit makes those\n  bearable, and one of them is the fastest way to make somebody disable the whole layer.\n- **CSS lives under `.lms-sfx`, tokens only.** No literal near-black or near-white; `check:theme`.\n\n## The desktop\n\n`@leumas/os` sounds at its **store dispatch**, not at its components and not at its actions object —\nAlt-Tab, Escape and the tiling shortcut all dispatch directly from the keyboard handler, and so do the\naction-mapper and the agent. One table (`os/os.events.js`) therefore covers every path there is.\n\nTwo things that table has to know, and neither is obvious:\n\n- **A dispatch is not an event.** Escape dispatches three closes unconditionally; the reducer ignores\n  the ones that were not open. Unguarded, pressing Escape at an empty desktop made three sounds.\n- **Another tab's desktop is not yours.** The OS syncs over a BroadcastChannel and replays remote\n  actions through the same dispatch, so a window opened in another tab would have been audible here.\n\nThe desktop carries `data-sfx-hover` on its root, which is what gives it hovers at all: it paints its\nown vocabulary (`desktop-icon`, `taskbar-btn`, `start-menu-item`, `context-item`…) and none of it is\nin the kit's curated list. It also wraps itself in `SoundBoundary`, so it is heard standalone and does\nnot open a second mixer when embedded in Studio.\n\n```sh\npnpm --filter @leumas/os test    # the cue table, including everything that must stay SILENT\n```\n\n## The 3D world\n\nThe world does not use the mixer this package's provider owns. It uses `@leumas/game-3d/audio`'s\n**spatial** one — a voice pool with distance culling and stealing, which shipped finished and silent\nfor want of a bank — with `WORLD_PACK` rendered into it. The binding lives in the world's own lane,\nat `@leumas/features/src/nav-views/world/audio.js`, because the numbers in it (\"a landing above 11\nunits per second is the heavy one\") are facts about that world's movement constants, not about audio.\n\nWhat this package owes it: the bank, `sustain` rendering for the five loops, and one AudioContext —\n`useSound().context()` is handed to three so it does not open a second with its own gesture latch.\n\n```sh\npnpm check:world-audio    # the world's policy: footfall phase, engine loops, seams, culling\n```\n\n## Hearing it without a browser\n\n```sh\npnpm sfx:preview                              # sfx-preview.wav — all three packs, in order\npnpm sfx:preview --pack world --out D:/tmp/a.wav\n```\n\nSustained voices are written out four times over, because the only question worth asking of a loop is\nwhether it clicks at the join, and one repetition cannot answer it.\n\nRenders through the *same* `renderPack` the browser uses, so there is no second implementation to\ndrift. Reviewing a sound design as a diff of numbers is not reviewing it.\n\n## Tuning\n\n`pnpm dev`, then Dashboard → Profile → Sound (or Studio → Settings). Every voice has a play button\nand a `Tune` drawer; drag, listen, and `Copy row` puts the line on the clipboard in exactly the shape\n`packs.js` expects. Paste it back into the table and commit — that is the whole loop, and it is the\nonly honest way to decide whether a sound is satisfying.\n\n## Verify\n\n```sh\npnpm check:sound                   # the policy, against a fake clock and a silent backend\nnode ops/infra/scripts/check-boot.mjs  # this package is ~3.7KB gzip eager; keep it there\npnpm --filter @leumas/game-3d test # the shared renderer did not change the game's bank\npnpm check:deck                    # the bundle guard this package is designed around\npnpm check:theme\n```\n",
  "source": {
    "path": "shared/packages/sound/README.md",
    "blobSha": "",
    "commit": "",
    "committedAt": "",
    "provenance": "no-git",
    "bytes": 9600,
    "hash": "b08a2144e2f7524c04e7586ab83930902b8d13ef"
  },
  "urls": {
    "html": "/p/packages/sound",
    "json": "/docs/packages/sound.json",
    "md": "/docs/packages/sound.md"
  },
  "links": {
    "composes": [
      "pkg:@leumas/game-kit",
      "pkg:@leumas/ui"
    ],
    "usedBy": [
      "pkg:@leumas/admin",
      "pkg:@leumas/features",
      "pkg:@leumas/game-3d",
      "pkg:@leumas/studio",
      "pkg:@leumas/web"
    ],
    "product": [
      "pkg:@leumas/admin",
      "pkg:@leumas/studio",
      "pkg:@leumas/web"
    ],
    "howTo": [],
    "skills": []
  },
  "exports": {
    "total": 40,
    "component": 22,
    "hook": 2,
    "helper": 16,
    "names": [
      {
        "n": "ALL_CATEGORIES",
        "k": "component"
      },
      {
        "n": "CATEGORIES",
        "k": "component"
      },
      {
        "n": "CUES",
        "k": "component"
      },
      {
        "n": "DEFAULT_CATEGORIES",
        "k": "component"
      },
      {
        "n": "DEFAULT_PACK",
        "k": "component"
      },
      {
        "n": "DEFAULTS",
        "k": "component"
      },
      {
        "n": "DEFAULTS",
        "k": "component"
      },
      {
        "n": "HUD_PACK",
        "k": "component"
      },
      {
        "n": "KEYS",
        "k": "component"
      },
      {
        "n": "KEYS",
        "k": "component"
      },
      {
        "n": "OS_PACK",
        "k": "component"
      },
      {
        "n": "PACK_IDS",
        "k": "component"
      },
      {
        "n": "PACKS",
        "k": "component"
      },
      {
        "n": "SfxHost",
        "k": "component"
      },
      {
        "n": "SoundBoundary",
        "k": "component"
      },
      {
        "n": "SoundCard",
        "k": "component"
      },
      {
        "n": "SoundProvider",
        "k": "component"
      },
      {
        "n": "SUSTAINED",
        "k": "component"
      },
      {
        "n": "UI_PACK_IDS",
        "k": "component"
      },
      {
        "n": "WORLD_CATEGORIES",
        "k": "component"
      },
      {
        "n": "WORLD_CUES",
        "k": "component"
      },
      {
        "n": "WORLD_PACK",
        "k": "component"
      },
      {
        "n": "useSound",
        "k": "hook"
      },
      {
        "n": "useSoundPack",
        "k": "hook"
      },
      {
        "n": "createMixer",
        "k": "helper"
      },
      {
        "n": "createSilentBackend",
        "k": "helper"
      },
      {
        "n": "createWebAudioBackend",
        "k": "helper"
      },
      {
        "n": "emitSfx",
        "k": "helper"
      },
      {
        "n": "emitSfx",
        "k": "helper"
      },
      {
        "n": "installSfx",
        "k": "helper"
      },
      {
        "n": "loadPacks",
        "k": "helper"
      },
      {
        "n": "mergeProfileSound",
        "k": "helper"
      },
      {
        "n": "mergeProfileSound",
        "k": "helper"
      },
      {
        "n": "readLocalSound",
        "k": "helper"
      },
      {
        "n": "readLocalSound",
        "k": "helper"
      },
      {
        "n": "renderPack",
        "k": "helper"
      },
      {
        "n": "renderVoice",
        "k": "helper"
      },
      {
        "n": "voiceFor",
        "k": "helper"
      },
      {
        "n": "writeLocalSound",
        "k": "helper"
      },
      {
        "n": "writeLocalSound",
        "k": "helper"
      }
    ]
  }
}
