npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

seslen

v0.1.0

Published

High-DX Web Audio: zero-dep, tree-shakeable library with synthesised UI sound presets, named buses + ducking, polyphony cap, throttle, jitter, fades, pan, sprites, sample-accurate scheduling, OfflineAudioContext render-to-WAV, AnalyserNode tap, prefers-re

Readme

[!IMPORTANT] 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/, copy _template.ts, and ship it. We'll help land it.

Why seslen?

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

import { createSeslen } from "seslen"
import { presets } from "seslen/presets"

const ses = createSeslen({ sources: presets })

await ses.play("victory") // play a preset
await ses.play("tick", { gain: 0.4 }) // gain / rate / detune / pan / fades / jitter
const handle = await ses.play("ambient", { loop: true })
handle?.fadeTo(0, 0.4) // ramp gain → 0 over 400 ms
handle?.stop()

Features

  • 🪶 Zero dependencies, pure ESM, tree-shakeable
  • 🎹 Synthesised UI presets — every play generated fresh on AudioContext
  • Lazy AudioContext — created only on the first play()
  • 🔓 Auto-unlock — resumes the context on the first user gesture
  • ♿️ Respects prefers-reduced-motion — auto-mutes by default
  • 💾 localStorage persistence for master volume + mute
  • 🎛 Per-call optionsgain, rate, detune, loop, pan, fadeIn, fadeOut, when, sprite, interrupt
  • 🌀 JitterrateJitter / gainJitter / detuneJitter so 100 ticks don't sound like 1 tick repeated
  • 🚦 Throttle per call — drop rapid-fire repeats inside a window
  • 🎚 Polyphony cap — per-source voices + steal: "oldest" | "newest" | "drop"
  • 🚌 Buses — named sub-mixers with their own volume / mute and ducking (sidechain)
  • Sample-accurate schedulingplay(name, { when: ses.now() + 0.25 })
  • 🪄 Single-flight cache for URL sources — decoded only once
  • 🧱 Three source types — URL, AudioBuffer, or your own SoundFactory
  • 📼 Render to WAV via OfflineAudioContextawait ses.render("victory")
  • 📈 Analyser tapses.analyser({ fftSize }) for waveforms / spectra
  • 🛡 SSR-safe — every method is a typed no-op via seslen/server
  • 🔡 Strict TypeScriptverbatimModuleSyntax, isolatedModules

Install

npm install seslen
# pnpm add seslen
# yarn add seslen
# bun add seslen

Quick start

1) Use the built-in presets

import { createSeslen } from "seslen"
import { presets, presetDefaults } from "seslen/presets"

const ses = createSeslen({
  sources: presets,
  defaults: presetDefaults, // per-preset jitter, throttle, voices
  volume: 0.8,
  persist: "seslen:master", // round-trip volume/mute through localStorage
})

button.addEventListener("click", () => ses.play("tick"))
form.addEventListener("submit", () => ses.play("success"))

2) Register a remote URL (with a sprite)

const ses = createSeslen({
  sources: { ui: "/sounds/ui-pack.mp3" },
})
await ses.play("ui", { sprite: [0, 0.08], gain: 0.6, rate: 1.2 })

3) Register your own SoundFactory

import { createSeslen, type SoundFactory } from "seslen"

const blip: SoundFactory = (ctx, master, opts) => {
  const t = ctx.currentTime
  const o = ctx.createOscillator()
  const g = ctx.createGain()
  o.frequency.setValueAtTime(880, t)
  g.gain.setValueAtTime(0.0001, t)
  g.gain.linearRampToValueAtTime(0.1 * (opts.gain ?? 1), t + 0.005)
  g.gain.exponentialRampToValueAtTime(0.0001, t + 0.12)
  o.connect(g).connect(master)
  o.start(t)
  o.stop(t + 0.14)
  let stopped = false
  return {
    stop() {
      stopped = true
      try {
        o.stop()
      } catch {}
    },
    get done() {
      return stopped
    },
    get duration() {
      return 0.14
    },
    onEnded() {},
  }
}

const ses = createSeslen({ sources: { blip } })
await ses.play("blip")

4) Buses + ducking

const ses = createSeslen({
  sources: presets,
  buses: { ui: {}, music: { volume: 0.6 } },
})

const music = await ses.play("ambient", { bus: "music", loop: true })

// Sidechain music down to 20% for 500 ms whenever the UI fires.
ses.on("play", (e) => {
  if (e.name !== "@pattern") ses.bus("music").duck({ target: 0.2, holdSeconds: 0.5 })
})

5) Throttle, jitter, interrupt — for sounds that fire often

// Hover sound: cap repeats, vary pitch slightly, never overlap with itself.
await ses.play("hover", {
  throttle: 40, // drop calls inside 40 ms
  rateJitter: 0.05, // ±5% pitch variation
  detuneJitter: 30, // ±30 cents
  interrupt: true, // stop any prior hover instance
})

6) Schedule a sequence

await ses.playPattern([
  { id: "tick" },
  { at: 80, id: "tick", options: { gain: 0.5 } },
  { at: 160, id: "success" },
])

// Sample-accurate one-off:
await ses.play("notify", { when: ses.now() + 0.25 })

7) Render a preset to a WAV file

const wav = await ses.render("victory", { durationSeconds: 1.5 })
const url = URL.createObjectURL(wav)
// download / share / preview

8) Visualise the master signal

const tap = ses.analyser({ fftSize: 256 })
const data = new Uint8Array(tap.fftSize / 2)
function frame() {
  tap.getSpectrum(data) // 0..255 per bin
  // draw bars …
  requestAnimationFrame(frame)
}
frame()

API

createSeslen(opts?: SeslenOptions): SeslenInstance

| option | type | default | description | | ---------------------- | ------------------------------------------------------ | ------- | ------------------------------------------------------------ | | sources | Record<string, SoundSource> | {} | Name → URL, AudioBuffer, or SoundFactory | | defaults | Partial<Record<string, SourceDefaults>> | {} | Per-source jitter / throttle / voices / steal / bus defaults | | volume | number (0..1) | 1 | Master gain | | buses | Record<string, { volume?: number; muted?: boolean }> | {} | Pre-declared named buses | | maxVoices | number | — | Global voice cap across all sources | | respectReducedMotion | boolean | true | Auto-mute when prefers-reduced-motion: reduce is set | | persist | string | — | localStorage key for master volume + mute persistence | | preload | boolean | false | Preload every URL source on first user gesture |

SeslenInstance

| method | returns | description | | ----------------------------------- | ------------------------------------------------------------ | ---------------------------------------------------------------- | | play(name, opts?) | Promise<PlayHandle \| null> | Play a sound. Returns null if throttled or dropped | | playPattern(steps) | Promise<PlayHandle> | Schedule a timed sequence; combined handle stops every step | | preload(name) | Promise<void> | Fetch + decode (URL sources only) | | stop(name) | number | Stop every active handle for one preset; returns the count | | stopAll() | void | Stop every active PlayHandle | | register(name, src, defs?) | void | Add or replace a source (with optional defaults) | | unregister(name) | boolean | Remove a source; stops live handles for that name | | has(name) / names() | boolean / string[] | Registry introspection | | getVolume() / setVolume() | number / void | Master gain accessors (clamped 0..1) | | mute() / unmute() / isMuted() | — | Master mute toggle | | bus(name) | BusHandle | Get or create a named sub-mixer (getVolume, mute, duck, …) | | now() / latency() | number / number | AudioContext.currentTime / baseLatency + outputLatency | | render(name, opts?) | Promise<Blob> | Render a sound to a 16-bit PCM WAV via OfflineAudioContext | | analyser(opts?) | AnalyserTap | Tap an AnalyserNode for waveform / spectrum data | | on(type, fn) / off() | () => void / void | Subscribe to play / ended / throttled / statechange | | pause() / resume() | Promise<void> | Suspend / resume the underlying context | | close() | Promise<void> | Close the AudioContext, clear caches, drop analyser | | isReady() / state() | boolean / "idle" \| "running" \| "suspended" \| "closed" | Lifecycle inspection |

PlayOptions

| field | type | default | description | | -------------- | -------------------- | ------- | ---------------------------------------------------------------- | | gain | number | 1 | Linear gain (0..1) | | rate | number | 1 | Playback rate (URL/AudioBuffer sources) | | detune | number | 0 | Detune in cents | | loop | boolean | false | Loop until stop() is called | | pan | number (-1..1) | 0 | Stereo pan via StereoPannerNode | | fadeIn | number (seconds) | 0 | Linear ramp in from silence | | fadeOut | number (seconds) | 0 | Linear ramp to silence on stop() | | when | number (seconds) | 0 | Schedule start at currentTime + when (use ses.now()) | | sprite | [offset, duration] | — | Slice of a buffer source | | interrupt | boolean | false | Stop every prior instance of the same sound first | | throttle | number (ms) | 0 | Drop the call if the same sound was triggered inside this window | | rateJitter | number (0..1) | 0 | ± random multiplier applied to rate | | gainJitter | number (0..1) | 0 | ± random multiplier applied to gain | | detuneJitter | number (cents) | 0 | ± random offset applied to detune | | bus | string | — | Route through a named bus instead of master |

PlayHandle

interface PlayHandle {
  stop(): void
  readonly done: boolean
  readonly duration: number | null
  onEnded(cb: () => void): void
  fadeTo?(value: number, seconds: number): void
  setGain?(value: number): void
  rampRate?(value: number, seconds: number): void
}

SourceDefaults

Set per-preset defaults via createSeslen({ defaults }) or register(name, source, defaults). Any per-call PlayOptions override these.

interface SourceDefaults {
  gain?: number
  rate?: number
  detune?: number
  pan?: number
  rateJitter?: number
  gainJitter?: number
  detuneJitter?: number
  minInterval?: number // throttle ms
  voices?: number // polyphony cap
  steal?: "oldest" | "newest" | "drop"
  bus?: string
}

BusHandle

interface BusHandle {
  readonly name: string
  getVolume(): number
  setVolume(value: number): void
  mute(): void
  unmute(): void
  isMuted(): boolean
  duck(opts: {
    target: number
    holdSeconds: number
    attackSeconds?: number
    releaseSeconds?: number
  }): void
}

SoundFactory

type SoundFactory = (ctx: AudioContext, destination: AudioNode, opts: PlayOptions) => PlayHandle

The destination is a bus or the master gain — connect your last node to it.

Built-in presets

import { presets, presetEntries, presetDefaults, presetTags } from "seslen/presets"

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.

Original eight

| name | tags | recipe | | --------- | ---------------------------- | --------------------------------- | | tick | ui feedback click | sine 4 kHz · 3 ms | | success | feedback success chirp | triangle 660→1320 Hz · 320 ms | | error | feedback error | square 220→150 Hz · 260 ms | | warning | feedback warning | square 880↔660 Hz · 500 ms | | message | notification bell | sine 880 + 1320 Hz · 420 ms | | add | ui feedback chirp | sine 880→1480 Hz · 140 ms | | delete | ui noise sweep | noise sweep 4 kHz→400 Hz · 200 ms | | victory | game success arpeggio | C-E-G-C arpeggio · 360 ms |

UI feedback

| name | tags | recipe | | ------------- | ------------------------ | ----------------------------------- | | hover | ui hover | sine 2.4 kHz · 25 ms | | pop | ui feedback | triangle 1200→320 Hz · 90 ms | | swoosh | ui noise sweep | noise bandpass 400→4000 Hz · 240 ms | | toggle-on | ui feedback toggle | sine 700 + 1100 Hz · 110 ms | | toggle-off | ui feedback toggle | sine 1100 + 700 Hz · 110 ms | | notify | notification chirp | sine 660-880-1320 Hz · 360 ms | | keypress | ui click keyboard | square 1.8 kHz · 12 ms | | scroll-tick | ui click | triangle 3 kHz · 6 ms | | drag | ui drag | sine 440→660 Hz · 120 ms | | drop | ui drag | sine 220→110 Hz · 120 ms | | expand | ui transition | sine 330→990 Hz · 200 ms | | collapse | ui transition | sine 990→330 Hz · 200 ms | | undo | ui feedback | triangle 880→520 Hz · 180 ms | | redo | ui feedback | triangle 520→880 Hz · 180 ms | | send | ui noise sweep | noise highpass 600→4000 Hz · 220 ms | | receive | notification chirp | sine 1320→880 Hz · 220 ms | | copy | ui feedback | sine 1480 + 1480 Hz · 90 ms | | paste | ui feedback | sine 880 Hz · 80 ms |

Game / playful

| name | tags | recipe | | ----------- | --------------------------- | ------------------------------------ | | level-up | game success arpeggio | C-D-E-G-C arpeggio · 480 ms | | coin | game pickup | square 988 + 1320 Hz · 180 ms | | jump | game | square 220→880 Hz · 100 ms | | shoot | game noise | noise bandpass 5 kHz→500 Hz · 130 ms | | explosion | game noise | noise lowpass 2 kHz→100 Hz · 600 ms |

Ambient / state

| name | tags | recipe | | ------------ | ----------------------- | ------------------------------------- | | heartbeat | ambient rhythm | sine 60 Hz double-thump · 600 ms | | alarm | feedback warning | square 880↔660 Hz · 4 cycles · 800 ms | | typewriter | ui click | triangle 2.6 kHz · 8 ms | | lock | ui feedback click | square 320 + 220 Hz · 140 ms | | unlock | ui feedback click | triangle 220 + 440 Hz · 140 ms |

Contributing presets

PRs that add new presets are very welcome. Every preset is one self-contained file under src/presets/ with a metadata header — see src/presets/CONTRIBUTING.md for a 30-line template and review checklist.

The Vite/Tailwind playground in web/ auto-detects every preset, with search + tag filters — your contribution shows up the moment it's wired into presets/index.ts.

SSR

// server.ts
import { createSeslen } from "seslen/server"
const ses = createSeslen() // every method is a typed no-op

Errors

import { SeslenError, ContextNotReadyError, DecodeError, LoadError } from "seslen"

SeslenError is the base. Use instanceof for targeted recovery.

Auto-unlock + accessibility

Browsers 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.

When 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.

License

MIT © productdevbook


💖 Support

If seslen saves you engineering time, consider sponsoring on GitHub.