---
title: "Sound"
description: "Synthesized UI feedback sounds, off by default."
category: Foundations
status: beta
since: 0.6.0
tags: [sound, audio, feedback, utils]
syncStatus: reviewed
verified: true
---

## API Reference

| Name | Description |
|---|---|
| `configureSound({ enabled, volume })` | Toggle and master volume (0–1, clamped); returns the resulting config |
| `getSoundConfig()` | A copy of the current config |
| `playSound(type \| preset)` | Plays; returns whether it actually did |
| `sound.click()` / `.key()` / `.toggle()` / `.success()` / `.error()` / `.open()` / `.close()` | Shorthands |
| `isSoundSupported()` | Whether the browser has Web Audio |
| `disposeSound()` | Closes the graph; the next play rebuilds it |
| `SOUND_PRESETS` | The preset table; read its parameters or start a custom cue from one |

## Off by Default

```ts
import { configureSound, sound } from '@talex-touch/tuffex/utils'

configureSound({ enabled: true, volume: 0.6 })
sound.click()
```

- Until enabled, nothing plays and no `AudioContext` is created.
- The graph is built lazily: browsers suspend an `AudioContext` created outside a user gesture, so a page that never plays shouldn't hold one.

## Why Synthesis

A few oscillators and an envelope ship at zero bundle cost, stay crisp at any sample rate, and cannot 404. The trade-off is timbre: samples are richer.

## Presets

Seven, deliberately. Peaks and lengths are measured sample by sample through an `OfflineAudioContext`.

| Preset | Use | Peak | Length |
|---|---|---|---|
| `click` | Button press | 0.103 | 40ms |
| `key` | Per character in a field | **0.056** | 27ms |
| `toggle` | Switch or checkbox turning on | 0.089 | 65ms |
| `success` | Completed action (rising pair) | 0.090 | 188ms |
| `error` | Rejected action (falling pair) | 0.100 | 216ms |
| `open` | Panel or dialog opening | 0.078 | 91ms |
| `close` | Panel or dialog closing | 0.077 | 91ms |

- `key` has the lowest peak: it fires per character, and at `click`'s level it becomes a typewriter.
- `open` and `close` mirror each other in peak and length, differing only in glide direction: direction carries the meaning, loudness does not.
- Nothing clips; every peak sits between 0.06 and 0.11.
- Tests enforce two length budgets: immediate feedback (`click` / `key` / `toggle`) ≤ 80ms, status cues (the rest) ≤ 250ms.

## Custom Cues

`playSound` also takes a patch built on the spot:

```ts
import { playSound } from '@talex-touch/tuffex/utils'

playSound({
  layers: [
    { wave: 'sine', freq: [400, 700], gain: 0.09, decay: 0.12 },
    { wave: 'sine', freq: 900, gain: 0.06, decay: 0.1, delay: 0.08 },
  ],
})
```

Two values in `freq` make a glide; `delay` offsets the second voice so the pair reads as a figure rather than a chord.

## Contract

- Never throws: disabled, unsupported, or before the browser has seen a gesture, `playSound` quietly returns `false`.
- A context parked by the autoplay policy is resumed on the next play; a rejected resume is swallowed.
- Each voice disconnects its own envelope when it ends, so a typing burst leaves no dead nodes on the master gain.
- The noise buffer is shared, not regenerated per keystroke.
- A volume change applies to the existing graph immediately, with no rebuild.

## Relationship to Vibration

`useVibrate` is the other feedback channel at this layer, with the same shape (preset table, main function, shorthand object). Don't fire sound and vibration for the same action: doubled feedback reads as a double trigger.

<TuffDocSourceLink />
