{"id":50606505,"url":"https://github.com/productdevbook/seslen","last_synced_at":"2026-06-05T23:31:11.663Z","repository":{"id":354844537,"uuid":"1224856077","full_name":"productdevbook/seslen","owner":"productdevbook","description":"Zero-dep, tree-shakeable Web Audio library with synthesised UI presets, buses + ducking, polyphony cap, throttle, jitter, fades, pan, sprites, OfflineAudioContext render-to-WAV, AnalyserNode tap, prefers-reduced-motion + SSR-safe. Strict TypeScript.","archived":false,"fork":false,"pushed_at":"2026-04-30T11:51:38.000Z","size":787,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-30T13:20:03.705Z","etag":null,"topics":["audiocontext","ducking","esm","feedback","game-audio","notification","polyphony","preset","react","sfx","sound-effects","ssr","synthesis","tree-shakeable","typescript","ui-sounds","vue","web-audio","webaudio","zero-dependency"],"latest_commit_sha":null,"homepage":"https://seslen.productdevbook.com","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/productdevbook.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null},"funding":{"github":["productdevbook"]}},"created_at":"2026-04-29T17:42:51.000Z","updated_at":"2026-04-30T12:39:20.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/productdevbook/seslen","commit_stats":null,"previous_names":["productdevbook/seslen"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/productdevbook/seslen","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/productdevbook%2Fseslen","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/productdevbook%2Fseslen/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/productdevbook%2Fseslen/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/productdevbook%2Fseslen/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/productdevbook","download_url":"https://codeload.github.com/productdevbook/seslen/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/productdevbook%2Fseslen/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33964367,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-05T02:00:06.157Z","response_time":120,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["audiocontext","ducking","esm","feedback","game-audio","notification","polyphony","preset","react","sfx","sound-effects","ssr","synthesis","tree-shakeable","typescript","ui-sounds","vue","web-audio","webaudio","zero-dependency"],"created_at":"2026-06-05T23:31:10.990Z","updated_at":"2026-06-05T23:31:11.654Z","avatar_url":"https://github.com/productdevbook.png","language":"TypeScript","funding_links":["https://github.com/sponsors/productdevbook"],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003cimg src=\"./.github/assets/cover.svg\" alt=\"seslen — High-DX Web Audio. Built-in UI sounds, one line away.\" width=\"100%\" /\u003e\n\u003c/p\u003e\n\n\u003ch1 align=\"center\"\u003eseslen\u003c/h1\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003cstrong\u003eHigh-DX Web Audio.\u003c/strong\u003e A small, ergonomic API on top of \u003ccode\u003eAudioContext\u003c/code\u003e with built-in, \u003cem\u003esynthesised\u003c/em\u003e UI sound presets.\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://www.npmjs.com/package/seslen\"\u003e\u003cimg src=\"https://img.shields.io/npm/v/seslen?color=42b883\" alt=\"npm\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://bundlephobia.com/package/seslen\"\u003e\u003cimg src=\"https://img.shields.io/bundlephobia/minzip/seslen?color=7073e8\" alt=\"bundle size\"\u003e\u003c/a\u003e\n  \u003ca href=\"./LICENSE\"\u003e\u003cimg src=\"https://img.shields.io/npm/l/seslen?color=ff8a65\" alt=\"license\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://seslen.productdevbook.com\"\u003e\u003cimg src=\"https://img.shields.io/badge/playground-seslen.productdevbook.com-2563eb\" alt=\"playground\"\u003e\u003c/a\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003cstrong\u003e🎧 Live playground:\u003c/strong\u003e \u003ca href=\"https://seslen.productdevbook.com\"\u003eseslen.productdevbook.com\u003c/a\u003e — preview every preset, build patterns in the composer, and copy the exact code.\n\u003c/p\u003e\n\n\u003e [!IMPORTANT]\n\u003e **Got a sound in your head? Send it our way.** `seslen` is community-built — every preset starts as a one-file PR. The biggest contribution you can make right now is a new preset: open [`src/presets/`](./src/presets/), copy [`_template.ts`](./src/presets/_template.ts), and ship it. We'll help land it.\n\n## Why seslen?\n\n`AudioContext` is powerful but low-level: context unlock, decode, cache, gain, source lifetime, polyphony, ducking — all manual. **`seslen`** wraps that in a one-line API and ships with **synthesised** UI presets (no audio files, no network, no decode):\n\n```ts\nimport { createSeslen } from \"seslen\"\nimport { presets } from \"seslen/presets\"\n\nconst ses = createSeslen({ sources: presets })\n\nawait ses.play(\"victory\") // play a preset\nawait ses.play(\"tick\", { gain: 0.4 }) // gain / rate / detune / pan / fades / jitter\nconst handle = await ses.play(\"ambient\", { loop: true })\nhandle?.fadeTo(0, 0.4) // ramp gain → 0 over 400 ms\nhandle?.stop()\n```\n\n## Features\n\n- 🪶 **Zero dependencies**, pure ESM, tree-shakeable\n- 🎹 **Synthesised UI presets** — every play generated fresh on `AudioContext`\n- ⚡ **Lazy AudioContext** — created only on the first `play()`\n- 🔓 **Auto-unlock** — resumes the context on the first user gesture\n- ♿️ **Respects `prefers-reduced-motion`** — auto-mutes by default\n- 💾 **`localStorage` persistence** for master volume + mute\n- 🎛 **Per-call options** — `gain`, `rate`, `detune`, `loop`, `pan`, `fadeIn`, `fadeOut`, `when`, `sprite`, `interrupt`\n- 🌀 **Jitter** — `rateJitter` / `gainJitter` / `detuneJitter` so 100 ticks don't sound like 1 tick repeated\n- 🚦 **Throttle** per call — drop rapid-fire repeats inside a window\n- 🎚 **Polyphony cap** — per-source `voices` + `steal: \"oldest\" | \"newest\" | \"drop\"`\n- 🚌 **Buses** — named sub-mixers with their own `volume` / `mute` and **ducking** (sidechain)\n- ⏱ **Sample-accurate scheduling** — `play(name, { when: ses.now() + 0.25 })`\n- 🪄 **Single-flight cache** for URL sources — decoded only once\n- 🧱 **Three source types** — URL, `AudioBuffer`, or your own `SoundFactory`\n- 📼 **Render to WAV** via `OfflineAudioContext` — `await ses.render(\"victory\")`\n- 📈 **Analyser tap** — `ses.analyser({ fftSize })` for waveforms / spectra\n- 🛡 **SSR-safe** — every method is a typed no-op via `seslen/server`\n- 🔡 **Strict TypeScript** — `verbatimModuleSyntax`, `isolatedModules`\n\n## Install\n\n```bash\nnpm install seslen\n# pnpm add seslen\n# yarn add seslen\n# bun add seslen\n```\n\n## Quick start\n\n### 1) Use the built-in presets\n\n```ts\nimport { createSeslen } from \"seslen\"\nimport { presets, presetDefaults } from \"seslen/presets\"\n\nconst ses = createSeslen({\n  sources: presets,\n  defaults: presetDefaults, // per-preset jitter, throttle, voices\n  volume: 0.8,\n  persist: \"seslen:master\", // round-trip volume/mute through localStorage\n})\n\nbutton.addEventListener(\"click\", () =\u003e ses.play(\"tick\"))\nform.addEventListener(\"submit\", () =\u003e ses.play(\"success\"))\n```\n\n### 2) Register a remote URL (with a sprite)\n\n```ts\nconst ses = createSeslen({\n  sources: { ui: \"/sounds/ui-pack.mp3\" },\n})\nawait ses.play(\"ui\", { sprite: [0, 0.08], gain: 0.6, rate: 1.2 })\n```\n\n### 3) Register your own `SoundFactory`\n\n```ts\nimport { createSeslen, type SoundFactory } from \"seslen\"\n\nconst blip: SoundFactory = (ctx, master, opts) =\u003e {\n  const t = ctx.currentTime\n  const o = ctx.createOscillator()\n  const g = ctx.createGain()\n  o.frequency.setValueAtTime(880, t)\n  g.gain.setValueAtTime(0.0001, t)\n  g.gain.linearRampToValueAtTime(0.1 * (opts.gain ?? 1), t + 0.005)\n  g.gain.exponentialRampToValueAtTime(0.0001, t + 0.12)\n  o.connect(g).connect(master)\n  o.start(t)\n  o.stop(t + 0.14)\n  let stopped = false\n  return {\n    stop() {\n      stopped = true\n      try {\n        o.stop()\n      } catch {}\n    },\n    get done() {\n      return stopped\n    },\n    get duration() {\n      return 0.14\n    },\n    onEnded() {},\n  }\n}\n\nconst ses = createSeslen({ sources: { blip } })\nawait ses.play(\"blip\")\n```\n\n### 4) Buses + ducking\n\n```ts\nconst ses = createSeslen({\n  sources: presets,\n  buses: { ui: {}, music: { volume: 0.6 } },\n})\n\nconst music = await ses.play(\"ambient\", { bus: \"music\", loop: true })\n\n// Sidechain music down to 20% for 500 ms whenever the UI fires.\nses.on(\"play\", (e) =\u003e {\n  if (e.name !== \"@pattern\") ses.bus(\"music\").duck({ target: 0.2, holdSeconds: 0.5 })\n})\n```\n\n### 5) Throttle, jitter, interrupt — for sounds that fire often\n\n```ts\n// Hover sound: cap repeats, vary pitch slightly, never overlap with itself.\nawait ses.play(\"hover\", {\n  throttle: 40, // drop calls inside 40 ms\n  rateJitter: 0.05, // ±5% pitch variation\n  detuneJitter: 30, // ±30 cents\n  interrupt: true, // stop any prior hover instance\n})\n```\n\n### 6) Schedule a sequence\n\n```ts\nawait ses.playPattern([\n  { id: \"tick\" },\n  { at: 80, id: \"tick\", options: { gain: 0.5 } },\n  { at: 160, id: \"success\" },\n])\n\n// Sample-accurate one-off:\nawait ses.play(\"notify\", { when: ses.now() + 0.25 })\n```\n\n### 7) Render a preset to a WAV file\n\n```ts\nconst wav = await ses.render(\"victory\", { durationSeconds: 1.5 })\nconst url = URL.createObjectURL(wav)\n// download / share / preview\n```\n\n### 8) Visualise the master signal\n\n```ts\nconst tap = ses.analyser({ fftSize: 256 })\nconst data = new Uint8Array(tap.fftSize / 2)\nfunction frame() {\n  tap.getSpectrum(data) // 0..255 per bin\n  // draw bars …\n  requestAnimationFrame(frame)\n}\nframe()\n```\n\n## API\n\n### `createSeslen(opts?: SeslenOptions): SeslenInstance`\n\n| option                 | type                                                   | default | description                                                  |\n| ---------------------- | ------------------------------------------------------ | ------- | ------------------------------------------------------------ |\n| `sources`              | `Record\u003cstring, SoundSource\u003e`                          | `{}`    | Name → URL, `AudioBuffer`, or `SoundFactory`                 |\n| `defaults`             | `Partial\u003cRecord\u003cstring, SourceDefaults\u003e\u003e`              | `{}`    | Per-source jitter / throttle / voices / steal / bus defaults |\n| `volume`               | `number` (0..1)                                        | `1`     | Master gain                                                  |\n| `buses`                | `Record\u003cstring, { volume?: number; muted?: boolean }\u003e` | `{}`    | Pre-declared named buses                                     |\n| `maxVoices`            | `number`                                               | —       | Global voice cap across all sources                          |\n| `respectReducedMotion` | `boolean`                                              | `true`  | Auto-mute when `prefers-reduced-motion: reduce` is set       |\n| `persist`              | `string`                                               | —       | `localStorage` key for master volume + mute persistence      |\n| `preload`              | `boolean`                                              | `false` | Preload every URL source on first user gesture               |\n\n### `SeslenInstance`\n\n| method                              | returns                                                      | description                                                      |\n| ----------------------------------- | ------------------------------------------------------------ | ---------------------------------------------------------------- |\n| `play(name, opts?)`                 | `Promise\u003cPlayHandle \\| null\u003e`                                | Play a sound. Returns `null` if throttled or dropped             |\n| `playPattern(steps)`                | `Promise\u003cPlayHandle\u003e`                                        | Schedule a timed sequence; combined handle stops every step      |\n| `preload(name)`                     | `Promise\u003cvoid\u003e`                                              | Fetch + decode (URL sources only)                                |\n| `stop(name)`                        | `number`                                                     | Stop every active handle for one preset; returns the count       |\n| `stopAll()`                         | `void`                                                       | Stop every active `PlayHandle`                                   |\n| `register(name, src, defs?)`        | `void`                                                       | Add or replace a source (with optional defaults)                 |\n| `unregister(name)`                  | `boolean`                                                    | Remove a source; stops live handles for that name                |\n| `has(name)` / `names()`             | `boolean` / `string[]`                                       | Registry introspection                                           |\n| `getVolume()` / `setVolume()`       | `number` / `void`                                            | Master gain accessors (clamped 0..1)                             |\n| `mute()` / `unmute()` / `isMuted()` | —                                                            | Master mute toggle                                               |\n| `bus(name)`                         | `BusHandle`                                                  | Get or create a named sub-mixer (`getVolume`, `mute`, `duck`, …) |\n| `now()` / `latency()`               | `number` / `number`                                          | `AudioContext.currentTime` / `baseLatency + outputLatency`       |\n| `render(name, opts?)`               | `Promise\u003cBlob\u003e`                                              | Render a sound to a 16-bit PCM WAV via `OfflineAudioContext`     |\n| `analyser(opts?)`                   | `AnalyserTap`                                                | Tap an `AnalyserNode` for waveform / spectrum data               |\n| `on(type, fn)` / `off()`            | `() =\u003e void` / `void`                                        | Subscribe to `play` / `ended` / `throttled` / `statechange`      |\n| `pause()` / `resume()`              | `Promise\u003cvoid\u003e`                                              | Suspend / resume the underlying context                          |\n| `close()`                           | `Promise\u003cvoid\u003e`                                              | Close the `AudioContext`, clear caches, drop analyser            |\n| `isReady()` / `state()`             | `boolean` / `\"idle\" \\| \"running\" \\| \"suspended\" \\| \"closed\"` | Lifecycle inspection                                             |\n\n### `PlayOptions`\n\n| field          | type                 | default | description                                                      |\n| -------------- | -------------------- | ------- | ---------------------------------------------------------------- |\n| `gain`         | `number`             | `1`     | Linear gain (0..1)                                               |\n| `rate`         | `number`             | `1`     | Playback rate (URL/`AudioBuffer` sources)                        |\n| `detune`       | `number`             | `0`     | Detune in cents                                                  |\n| `loop`         | `boolean`            | `false` | Loop until `stop()` is called                                    |\n| `pan`          | `number` (-1..1)     | `0`     | Stereo pan via `StereoPannerNode`                                |\n| `fadeIn`       | `number` (seconds)   | `0`     | Linear ramp in from silence                                      |\n| `fadeOut`      | `number` (seconds)   | `0`     | Linear ramp to silence on `stop()`                               |\n| `when`         | `number` (seconds)   | `0`     | Schedule start at `currentTime + when` (use `ses.now()`)         |\n| `sprite`       | `[offset, duration]` | —       | Slice of a buffer source                                         |\n| `interrupt`    | `boolean`            | `false` | Stop every prior instance of the same sound first                |\n| `throttle`     | `number` (ms)        | `0`     | Drop the call if the same sound was triggered inside this window |\n| `rateJitter`   | `number` (0..1)      | `0`     | ± random multiplier applied to `rate`                            |\n| `gainJitter`   | `number` (0..1)      | `0`     | ± random multiplier applied to `gain`                            |\n| `detuneJitter` | `number` (cents)     | `0`     | ± random offset applied to `detune`                              |\n| `bus`          | `string`             | —       | Route through a named bus instead of master                      |\n\n### `PlayHandle`\n\n```ts\ninterface PlayHandle {\n  stop(): void\n  readonly done: boolean\n  readonly duration: number | null\n  onEnded(cb: () =\u003e void): void\n  fadeTo?(value: number, seconds: number): void\n  setGain?(value: number): void\n  rampRate?(value: number, seconds: number): void\n}\n```\n\n### `SourceDefaults`\n\nSet per-preset defaults via `createSeslen({ defaults })` or `register(name, source, defaults)`. Any per-call `PlayOptions` override these.\n\n```ts\ninterface SourceDefaults {\n  gain?: number\n  rate?: number\n  detune?: number\n  pan?: number\n  rateJitter?: number\n  gainJitter?: number\n  detuneJitter?: number\n  minInterval?: number // throttle ms\n  voices?: number // polyphony cap\n  steal?: \"oldest\" | \"newest\" | \"drop\"\n  bus?: string\n}\n```\n\n### `BusHandle`\n\n```ts\ninterface BusHandle {\n  readonly name: string\n  getVolume(): number\n  setVolume(value: number): void\n  mute(): void\n  unmute(): void\n  isMuted(): boolean\n  duck(opts: {\n    target: number\n    holdSeconds: number\n    attackSeconds?: number\n    releaseSeconds?: number\n  }): void\n}\n```\n\n### `SoundFactory`\n\n```ts\ntype SoundFactory = (ctx: AudioContext, destination: AudioNode, opts: PlayOptions) =\u003e PlayHandle\n```\n\nThe `destination` is a bus or the master gain — connect your last node to it.\n\n## Built-in presets\n\n```ts\nimport { presets, presetEntries, presetDefaults, presetTags } from \"seslen/presets\"\n```\n\n`presets` is the plug-and-play factory map for `createSeslen`. `presetEntries` carries the same factories with metadata (label, description, tags, recipe, motion hint, accent colour, author, defaults). `presetDefaults` is the per-preset jitter/throttle/voices map — pass it to `createSeslen({ defaults })` for sensible baselines. `presetTags` is the deduplicated tag union.\n\n### Original eight\n\n| name      | tags                         | recipe                            |\n| --------- | ---------------------------- | --------------------------------- |\n| `tick`    | `ui` `feedback` `click`      | sine 4 kHz · 3 ms                 |\n| `success` | `feedback` `success` `chirp` | triangle 660→1320 Hz · 320 ms     |\n| `error`   | `feedback` `error`           | square 220→150 Hz · 260 ms        |\n| `warning` | `feedback` `warning`         | square 880↔660 Hz · 500 ms        |\n| `message` | `notification` `bell`        | sine 880 + 1320 Hz · 420 ms       |\n| `add`     | `ui` `feedback` `chirp`      | sine 880→1480 Hz · 140 ms         |\n| `delete`  | `ui` `noise` `sweep`         | noise sweep 4 kHz→400 Hz · 200 ms |\n| `victory` | `game` `success` `arpeggio`  | C-E-G-C arpeggio · 360 ms         |\n\n### UI feedback\n\n| name          | tags                     | recipe                              |\n| ------------- | ------------------------ | ----------------------------------- |\n| `hover`       | `ui` `hover`             | sine 2.4 kHz · 25 ms                |\n| `pop`         | `ui` `feedback`          | triangle 1200→320 Hz · 90 ms        |\n| `swoosh`      | `ui` `noise` `sweep`     | noise bandpass 400→4000 Hz · 240 ms |\n| `toggle-on`   | `ui` `feedback` `toggle` | sine 700 + 1100 Hz · 110 ms         |\n| `toggle-off`  | `ui` `feedback` `toggle` | sine 1100 + 700 Hz · 110 ms         |\n| `notify`      | `notification` `chirp`   | sine 660-880-1320 Hz · 360 ms       |\n| `keypress`    | `ui` `click` `keyboard`  | square 1.8 kHz · 12 ms              |\n| `scroll-tick` | `ui` `click`             | triangle 3 kHz · 6 ms               |\n| `drag`        | `ui` `drag`              | sine 440→660 Hz · 120 ms            |\n| `drop`        | `ui` `drag`              | sine 220→110 Hz · 120 ms            |\n| `expand`      | `ui` `transition`        | sine 330→990 Hz · 200 ms            |\n| `collapse`    | `ui` `transition`        | sine 990→330 Hz · 200 ms            |\n| `undo`        | `ui` `feedback`          | triangle 880→520 Hz · 180 ms        |\n| `redo`        | `ui` `feedback`          | triangle 520→880 Hz · 180 ms        |\n| `send`        | `ui` `noise` `sweep`     | noise highpass 600→4000 Hz · 220 ms |\n| `receive`     | `notification` `chirp`   | sine 1320→880 Hz · 220 ms           |\n| `copy`        | `ui` `feedback`          | sine 1480 + 1480 Hz · 90 ms         |\n| `paste`       | `ui` `feedback`          | sine 880 Hz · 80 ms                 |\n\n### Game / playful\n\n| name        | tags                        | recipe                               |\n| ----------- | --------------------------- | ------------------------------------ |\n| `level-up`  | `game` `success` `arpeggio` | C-D-E-G-C arpeggio · 480 ms          |\n| `coin`      | `game` `pickup`             | square 988 + 1320 Hz · 180 ms        |\n| `jump`      | `game`                      | square 220→880 Hz · 100 ms           |\n| `shoot`     | `game` `noise`              | noise bandpass 5 kHz→500 Hz · 130 ms |\n| `explosion` | `game` `noise`              | noise lowpass 2 kHz→100 Hz · 600 ms  |\n\n### Ambient / state\n\n| name         | tags                    | recipe                                |\n| ------------ | ----------------------- | ------------------------------------- |\n| `heartbeat`  | `ambient` `rhythm`      | sine 60 Hz double-thump · 600 ms      |\n| `alarm`      | `feedback` `warning`    | square 880↔660 Hz · 4 cycles · 800 ms |\n| `typewriter` | `ui` `click`            | triangle 2.6 kHz · 8 ms               |\n| `lock`       | `ui` `feedback` `click` | square 320 + 220 Hz · 140 ms          |\n| `unlock`     | `ui` `feedback` `click` | triangle 220 + 440 Hz · 140 ms        |\n\n## Contributing presets\n\nPRs that add new presets are very welcome. Every preset is one self-contained file under [`src/presets/`](./src/presets/) with a metadata header — see [`src/presets/CONTRIBUTING.md`](./src/presets/CONTRIBUTING.md) for a 30-line template and review checklist.\n\nThe Vite/Tailwind playground in [`web/`](./web/) auto-detects every preset, with search + tag filters — your contribution shows up the moment it's wired into `presets/index.ts`.\n\n## SSR\n\n```ts\n// server.ts\nimport { createSeslen } from \"seslen/server\"\nconst ses = createSeslen() // every method is a typed no-op\n```\n\n## Errors\n\n```ts\nimport { SeslenError, ContextNotReadyError, DecodeError, LoadError } from \"seslen\"\n```\n\n`SeslenError` is the base. Use `instanceof` for targeted recovery.\n\n## Auto-unlock + accessibility\n\nBrowsers keep `AudioContext` `suspended` until a user gesture. `seslen` calls `resume()` on the first `pointerdown` / `keydown` / `touchstart`, then detaches the listeners — you never deal with it.\n\nWhen the user has `prefers-reduced-motion: reduce`, `seslen` auto-mutes the master bus (this is the default — opt out with `respectReducedMotion: false`). The setting is re-evaluated live.\n\n## License\n\n[MIT](./LICENSE) © [productdevbook](https://github.com/productdevbook)\n\n---\n\n### 💖 Support\n\nIf `seslen` saves you engineering time, consider [sponsoring on GitHub](https://github.com/sponsors/productdevbook).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fproductdevbook%2Fseslen","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fproductdevbook%2Fseslen","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fproductdevbook%2Fseslen/lists"}