earcons 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 smolgroot
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,169 @@
1
+ # earcon
2
+
3
+ > Tiny vanilla TypeScript library for UI notification sounds — pure Web Audio API, no audio files, zero dependencies.
4
+
5
+ [![npm](https://img.shields.io/npm/v/earcons)](https://www.npmjs.com/package/earcons)
6
+ [![bundle size](https://img.shields.io/bundlephobia/minzip/earcons)](https://bundlephobia.com/package/earcons)
7
+ [![license](https://img.shields.io/npm/l/earcons)](LICENSE)
8
+
9
+ ## Why earcon?
10
+
11
+ | | earcon | Howler.js | Tone.js | ZzFX |
12
+ |---|:---:|:---:|:---:|:---:|
13
+ | **No audio files** | ✅ | ❌ | ✅ | ✅ |
14
+ | **Notification-specific API** | ✅ | ❌ | ❌ | ❌ |
15
+ | **TypeScript native** | ✅ | via @types | ✅ | ❌ |
16
+ | **Zero dependencies** | ✅ | ✅ | ❌ | ✅ |
17
+ | **Bundle size** | ~3 KB | 318 KB | 5.4 MB | <1 KB |
18
+ | **Semantic sound names** | ✅ | ❌ | ❌ | ❌ |
19
+ | **Customizable** | ✅ | ✅ | ✅ | ✅ |
20
+
21
+ An [earcon](https://en.wikipedia.org/wiki/Earcon) is an auditory icon — a short, meaningful sound signal used in user interfaces. This library provides a curated set of them, generated entirely in code.
22
+
23
+ ---
24
+
25
+ ## Install
26
+
27
+ ```bash
28
+ npm install earcons
29
+ ```
30
+
31
+ ### CDN (no build step)
32
+
33
+ ```html
34
+ <!-- jsDelivr -->
35
+ <script src="https://cdn.jsdelivr.net/npm/earcons/dist/earcon.min.global.js"></script>
36
+
37
+ <!-- unpkg -->
38
+ <script src="https://unpkg.com/earcons/dist/earcon.min.global.js"></script>
39
+ ```
40
+
41
+ All functions are available under the global `Earcon` object:
42
+
43
+ ```html
44
+ <button onclick="Earcon.playSuccess()">✅</button>
45
+ <button onclick="Earcon.playError()">❌</button>
46
+ <script>
47
+ // or
48
+ Earcon.earcon("notification", { volume: 0.8 });
49
+ </script>
50
+ ```
51
+
52
+ ---
53
+
54
+ ## Quick start
55
+
56
+ ```ts
57
+ import { playSuccess, playError, playWarning } from "earcons";
58
+
59
+ // Defaults — medium variant, volume 0.5
60
+ await playSuccess();
61
+ await playError();
62
+ await playWarning();
63
+ ```
64
+
65
+ ---
66
+
67
+ ## API
68
+
69
+ ### Named play functions
70
+
71
+ Each function accepts an optional [`SoundOptions`](#soundoptions) object.
72
+
73
+ ```ts
74
+ import {
75
+ playSuccess,
76
+ playError,
77
+ playWarning,
78
+ playNotification,
79
+ playClick,
80
+ playInfo,
81
+ } from "earcons";
82
+
83
+ await playSuccess();
84
+ await playError({ variant: "long", volume: 0.8 });
85
+ await playWarning({ variant: "short" });
86
+ await playNotification({ pitch: 7 }); // +7 semitones
87
+ await playClick({ variant: "short" });
88
+ await playInfo({ volume: 0.3 });
89
+ ```
90
+
91
+ ### Generic `earcon()` — play by name
92
+
93
+ ```ts
94
+ import { earcon } from "earcons";
95
+
96
+ await earcon("success");
97
+ await earcon("error", { variant: "long" });
98
+ ```
99
+
100
+ **Built-in sound names:** `success` · `error` · `warning` · `notification` · `click` · `info`
101
+
102
+ ### Register a custom sound
103
+
104
+ ```ts
105
+ import { registerSound, earcon } from "earcons";
106
+ import type { SoundPreset } from "earcons";
107
+
108
+ const chirp: SoundPreset = (_variant, _pitch) => ({
109
+ name: "chirp",
110
+ duration: 0.15,
111
+ notes: [
112
+ { frequency: 1200, duration: 0.07, startAt: 0, waveShape: "sine" },
113
+ { frequency: 1600, duration: 0.08, startAt: 0.07, waveShape: "sine" },
114
+ ],
115
+ });
116
+
117
+ registerSound("chirp", chirp);
118
+ await earcon("chirp");
119
+ ```
120
+
121
+ ### `SoundOptions`
122
+
123
+ | Option | Type | Default | Description |
124
+ |---|---|---|---|
125
+ | `volume` | `number` | `0.5` | Master volume, 0–1 |
126
+ | `variant` | `"short" \| "medium" \| "long"` | `"medium"` | Duration variant |
127
+ | `pitch` | `number` | `0` | Semitone offset (±) |
128
+ | `audioContext` | `AudioContext` | shared singleton | Bring your own context |
129
+ | `onEnded` | `() => void` | — | Called when the sound finishes |
130
+
131
+ ### AudioContext management
132
+
133
+ ```ts
134
+ import { closeAudioContext, setAudioContext } from "earcons";
135
+
136
+ // Provide your own AudioContext (useful in frameworks)
137
+ const myCtx = new AudioContext();
138
+ setAudioContext(myCtx);
139
+
140
+ // Tear down (e.g. on unmount)
141
+ await closeAudioContext();
142
+ ```
143
+
144
+ ---
145
+
146
+ ## Sound reference
147
+
148
+ | Name | Waveform | Character |
149
+ |---|---|---|
150
+ | `success` | sine | Ascending major triad — bright, positive |
151
+ | `error` | sawtooth | Descending tritone — harsh, attention-grabbing |
152
+ | `warning` | triangle | Flat then descending minor second — cautious |
153
+ | `notification` | sine | Soft descending two-tone ding — neutral |
154
+ | `click` | sine | Very short transient — tactile UI feedback |
155
+ | `info` | sine | Single soft tone — informational, non-intrusive |
156
+
157
+ ---
158
+
159
+ ## Browser support
160
+
161
+ Any browser that supports the [Web Audio API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API) (all modern browsers since 2014).
162
+
163
+ > **Autoplay policy:** earcon automatically calls `AudioContext.resume()` before playing. Make sure you call a play function in response to a user gesture on first load, or the browser may suppress audio.
164
+
165
+ ---
166
+
167
+ ## License
168
+
169
+ MIT
@@ -0,0 +1 @@
1
+ "use strict";var Earcon=(()=>{var w=Object.defineProperty;var N=Object.getOwnPropertyDescriptor;var C=Object.getOwnPropertyNames;var R=Object.prototype.hasOwnProperty;var O=(e,n)=>{for(var o in n)w(e,o,{get:n[o],enumerable:!0})},b=(e,n,o,c)=>{if(n&&typeof n=="object"||typeof n=="function")for(let r of C(n))!R.call(e,r)&&r!==o&&w(e,r,{get:()=>n[r],enumerable:!(c=N(n,r))||c.enumerable});return e};var j=e=>b(w({},"__esModule",{value:!0}),e);var Y={};O(Y,{closeAudioContext:()=>V,earcon:()=>Q,playClick:()=>H,playError:()=>F,playInfo:()=>J,playNotification:()=>z,playSuccess:()=>D,playWarning:()=>U,registerSound:()=>X,semitonesToMultiplier:()=>q,setAudioContext:()=>P});var u=null;async function E(){return u||(u=new AudioContext),u.state==="suspended"&&await u.resume(),u}async function V(){u&&(await u.close(),u=null)}function P(e){u=e}var G={attack:.01,decay:.05,sustain:.7,release:.08,hold:0};function q(e){return Math.pow(2,e/12)}function I(e,n,o,c,r){let t={...G,...n.envelope},i=r+n.startAt,s=n.gain??1,m=n.waveShape??"sine",d=n.frequency*c,a=e.createGain();a.connect(o),a.gain.setValueAtTime(0,i),a.gain.linearRampToValueAtTime(s,i+t.attack);let x=i+t.attack+(t.hold??0);a.gain.setValueAtTime(s,x),a.gain.linearRampToValueAtTime(s*t.sustain,x+t.decay);let l=i+n.duration;a.gain.setValueAtTime(s*t.sustain,l),a.gain.linearRampToValueAtTime(0,l+t.release);let f=e.createOscillator();return f.type=m,f.frequency.setValueAtTime(d,i),f.connect(a),f.start(i),f.stop(l+t.release+.01),l+t.release}function k(e,n,o,c,r){let t=e.createGain();t.gain.setValueAtTime(Math.max(0,Math.min(1,o)),e.currentTime),t.connect(e.destination);let i=q(c),s=e.currentTime,m=s;for(let d of n.notes){let a=I(e,d,t,i,s);a>m&&(m=a)}if(r){let d=e.createOscillator(),a=e.createGain();a.gain.setValueAtTime(0,s),d.connect(a),a.connect(e.destination),d.start(s),d.stop(m),d.onended=r}}var M={short:{name:"success",duration:.25,notes:[{frequency:523.25,duration:.1,startAt:0,waveShape:"sine",gain:.8},{frequency:783.99,duration:.15,startAt:.1,waveShape:"sine",gain:.9}]},medium:{name:"success",duration:.45,notes:[{frequency:523.25,duration:.12,startAt:0,waveShape:"sine",gain:.7},{frequency:659.25,duration:.12,startAt:.12,waveShape:"sine",gain:.8},{frequency:783.99,duration:.2,startAt:.24,waveShape:"sine",gain:.9}]},long:{name:"success",duration:.7,notes:[{frequency:392,duration:.1,startAt:0,waveShape:"sine",gain:.6},{frequency:523.25,duration:.12,startAt:.1,waveShape:"sine",gain:.7},{frequency:659.25,duration:.12,startAt:.22,waveShape:"sine",gain:.8},{frequency:783.99,duration:.25,startAt:.34,waveShape:"sine",gain:.9}]}};function S(e,n){return M[e]}var B={short:{name:"error",duration:.25,notes:[{frequency:440,duration:.1,startAt:0,waveShape:"square",gain:.5,envelope:{attack:.005,decay:.04,sustain:.6,release:.06}},{frequency:311,duration:.15,startAt:.1,waveShape:"square",gain:.6,envelope:{attack:.005,decay:.04,sustain:.6,release:.06}}]},medium:{name:"error",duration:.5,notes:[{frequency:440,duration:.12,startAt:0,waveShape:"sawtooth",gain:.4,envelope:{attack:.005,decay:.06,sustain:.5,release:.08}},{frequency:392,duration:.12,startAt:.13,waveShape:"sawtooth",gain:.45,envelope:{attack:.005,decay:.06,sustain:.5,release:.08}},{frequency:311,duration:.2,startAt:.26,waveShape:"sawtooth",gain:.55,envelope:{attack:.005,decay:.06,sustain:.5,release:.12}}]},long:{name:"error",duration:.75,notes:[{frequency:440,duration:.12,startAt:0,waveShape:"sawtooth",gain:.4,envelope:{attack:.005,decay:.07,sustain:.5,release:.1}},{frequency:415,duration:.1,startAt:.14,waveShape:"sawtooth",gain:.4,envelope:{attack:.005,decay:.07,sustain:.5,release:.1}},{frequency:370,duration:.12,startAt:.26,waveShape:"sawtooth",gain:.45,envelope:{attack:.005,decay:.07,sustain:.5,release:.1}},{frequency:311,duration:.25,startAt:.4,waveShape:"sawtooth",gain:.55,envelope:{attack:.005,decay:.08,sustain:.5,release:.15}}]}};function y(e,n){return B[e]}var K={short:{name:"warning",duration:.28,notes:[{frequency:587.33,duration:.1,startAt:0,waveShape:"triangle",gain:.7},{frequency:523.25,duration:.18,startAt:.1,waveShape:"triangle",gain:.75}]},medium:{name:"warning",duration:.5,notes:[{frequency:587.33,duration:.12,startAt:0,waveShape:"triangle",gain:.65},{frequency:587.33,duration:.12,startAt:.14,waveShape:"triangle",gain:.65},{frequency:493.88,duration:.2,startAt:.28,waveShape:"triangle",gain:.75}]},long:{name:"warning",duration:.8,notes:[{frequency:587.33,duration:.12,startAt:0,waveShape:"triangle",gain:.6},{frequency:587.33,duration:.12,startAt:.15,waveShape:"triangle",gain:.6},{frequency:554.37,duration:.12,startAt:.3,waveShape:"triangle",gain:.65},{frequency:493.88,duration:.25,startAt:.45,waveShape:"triangle",gain:.75}]}};function v(e,n){return K[e]}var L={short:{name:"notification",duration:.22,notes:[{frequency:880,duration:.08,startAt:0,waveShape:"sine",gain:.6},{frequency:660,duration:.14,startAt:.08,waveShape:"sine",gain:.65}]},medium:{name:"notification",duration:.38,notes:[{frequency:880,duration:.1,startAt:0,waveShape:"sine",gain:.55},{frequency:660,duration:.28,startAt:.1,waveShape:"sine",gain:.6}]},long:{name:"notification",duration:.55,notes:[{frequency:1046.5,duration:.08,startAt:0,waveShape:"sine",gain:.5},{frequency:880,duration:.1,startAt:.1,waveShape:"sine",gain:.55},{frequency:660,duration:.3,startAt:.22,waveShape:"sine",gain:.6}]}};function g(e,n){return L[e]}var W={short:{name:"click",duration:.04,notes:[{frequency:1200,duration:.03,startAt:0,waveShape:"sine",gain:.5,envelope:{attack:.002,decay:.015,sustain:0,release:.01}}]},medium:{name:"click",duration:.07,notes:[{frequency:900,duration:.05,startAt:0,waveShape:"sine",gain:.55,envelope:{attack:.002,decay:.03,sustain:0,release:.015}}]},long:{name:"click",duration:.12,notes:[{frequency:700,duration:.08,startAt:0,waveShape:"triangle",gain:.6,envelope:{attack:.002,decay:.05,sustain:0,release:.025}}]}};function A(e,n){return W[e]}var _={short:{name:"info",duration:.2,notes:[{frequency:698.46,duration:.18,startAt:0,waveShape:"sine",gain:.55,envelope:{attack:.015,decay:.04,sustain:.5,release:.1}}]},medium:{name:"info",duration:.35,notes:[{frequency:698.46,duration:.32,startAt:0,waveShape:"sine",gain:.55,envelope:{attack:.02,decay:.05,sustain:.5,release:.12}}]},long:{name:"info",duration:.55,notes:[{frequency:587.33,duration:.18,startAt:0,waveShape:"sine",gain:.5,envelope:{attack:.02,decay:.05,sustain:.4,release:.1}},{frequency:698.46,duration:.3,startAt:.2,waveShape:"sine",gain:.55,envelope:{attack:.02,decay:.06,sustain:.5,release:.12}}]}};function h(e,n){return _[e]}async function p(e,n={}){let{volume:o=.5,pitch:c=0,audioContext:r,onEnded:t}=n,i=r??await E();k(i,e,o,c,t)}async function D(e={}){return p(S(e.variant??"medium",e.pitch??0),e)}async function F(e={}){return p(y(e.variant??"medium",e.pitch??0),e)}async function U(e={}){return p(v(e.variant??"medium",e.pitch??0),e)}async function z(e={}){return p(g(e.variant??"medium",e.pitch??0),e)}async function H(e={}){return p(A(e.variant??"short",e.pitch??0),e)}async function J(e={}){return p(h(e.variant??"medium",e.pitch??0),e)}var T={success:S,error:y,warning:v,notification:g,click:A,info:h};async function Q(e,n={}){let o=T[e];return p(o(n.variant??"medium",n.pitch??0),n)}function X(e,n){T[e]=n}return j(Y);})();
package/dist/index.cjs ADDED
@@ -0,0 +1,449 @@
1
+ 'use strict';
2
+
3
+ // src/context.ts
4
+ var _ctx = null;
5
+ async function getAudioContext() {
6
+ if (!_ctx) {
7
+ _ctx = new AudioContext();
8
+ }
9
+ if (_ctx.state === "suspended") {
10
+ await _ctx.resume();
11
+ }
12
+ return _ctx;
13
+ }
14
+ async function closeAudioContext() {
15
+ if (_ctx) {
16
+ await _ctx.close();
17
+ _ctx = null;
18
+ }
19
+ }
20
+ function setAudioContext(ctx) {
21
+ _ctx = ctx;
22
+ }
23
+
24
+ // src/synth.ts
25
+ var DEFAULT_ENVELOPE = {
26
+ attack: 0.01,
27
+ decay: 0.05,
28
+ sustain: 0.7,
29
+ release: 0.08,
30
+ hold: 0
31
+ };
32
+ function semitonesToMultiplier(semitones) {
33
+ return Math.pow(2, semitones / 12);
34
+ }
35
+ function scheduleNote(ctx, note, masterGain, pitchMultiplier, timeOffset) {
36
+ const env = {
37
+ ...DEFAULT_ENVELOPE,
38
+ ...note.envelope
39
+ };
40
+ const t0 = timeOffset + note.startAt;
41
+ const noteGain = note.gain ?? 1;
42
+ const waveShape = note.waveShape ?? "sine";
43
+ const freq = note.frequency * pitchMultiplier;
44
+ const gainNode = ctx.createGain();
45
+ gainNode.connect(masterGain);
46
+ gainNode.gain.setValueAtTime(0, t0);
47
+ gainNode.gain.linearRampToValueAtTime(noteGain, t0 + env.attack);
48
+ const holdEnd = t0 + env.attack + (env.hold ?? 0);
49
+ gainNode.gain.setValueAtTime(noteGain, holdEnd);
50
+ gainNode.gain.linearRampToValueAtTime(
51
+ noteGain * env.sustain,
52
+ holdEnd + env.decay
53
+ );
54
+ const noteEnd = t0 + note.duration;
55
+ gainNode.gain.setValueAtTime(noteGain * env.sustain, noteEnd);
56
+ gainNode.gain.linearRampToValueAtTime(0, noteEnd + env.release);
57
+ const osc = ctx.createOscillator();
58
+ osc.type = waveShape;
59
+ osc.frequency.setValueAtTime(freq, t0);
60
+ osc.connect(gainNode);
61
+ osc.start(t0);
62
+ osc.stop(noteEnd + env.release + 0.01);
63
+ return noteEnd + env.release;
64
+ }
65
+ function playSound(ctx, sound, volume, pitchSemitones, onEnded) {
66
+ const masterGain = ctx.createGain();
67
+ masterGain.gain.setValueAtTime(Math.max(0, Math.min(1, volume)), ctx.currentTime);
68
+ masterGain.connect(ctx.destination);
69
+ const pitchMultiplier = semitonesToMultiplier(pitchSemitones);
70
+ const timeOffset = ctx.currentTime;
71
+ let latestEnd = timeOffset;
72
+ for (const note of sound.notes) {
73
+ const end = scheduleNote(ctx, note, masterGain, pitchMultiplier, timeOffset);
74
+ if (end > latestEnd) latestEnd = end;
75
+ }
76
+ if (onEnded) {
77
+ const sentinel = ctx.createOscillator();
78
+ const silentGain = ctx.createGain();
79
+ silentGain.gain.setValueAtTime(0, timeOffset);
80
+ sentinel.connect(silentGain);
81
+ silentGain.connect(ctx.destination);
82
+ sentinel.start(timeOffset);
83
+ sentinel.stop(latestEnd);
84
+ sentinel.onended = onEnded;
85
+ }
86
+ }
87
+
88
+ // src/sounds/success.ts
89
+ var VARIANTS = {
90
+ short: {
91
+ name: "success",
92
+ duration: 0.25,
93
+ notes: [
94
+ { frequency: 523.25, duration: 0.1, startAt: 0, waveShape: "sine", gain: 0.8 },
95
+ { frequency: 783.99, duration: 0.15, startAt: 0.1, waveShape: "sine", gain: 0.9 }
96
+ ]
97
+ },
98
+ medium: {
99
+ name: "success",
100
+ duration: 0.45,
101
+ notes: [
102
+ { frequency: 523.25, duration: 0.12, startAt: 0, waveShape: "sine", gain: 0.7 },
103
+ { frequency: 659.25, duration: 0.12, startAt: 0.12, waveShape: "sine", gain: 0.8 },
104
+ { frequency: 783.99, duration: 0.2, startAt: 0.24, waveShape: "sine", gain: 0.9 }
105
+ ]
106
+ },
107
+ long: {
108
+ name: "success",
109
+ duration: 0.7,
110
+ notes: [
111
+ { frequency: 392, duration: 0.1, startAt: 0, waveShape: "sine", gain: 0.6 },
112
+ { frequency: 523.25, duration: 0.12, startAt: 0.1, waveShape: "sine", gain: 0.7 },
113
+ { frequency: 659.25, duration: 0.12, startAt: 0.22, waveShape: "sine", gain: 0.8 },
114
+ { frequency: 783.99, duration: 0.25, startAt: 0.34, waveShape: "sine", gain: 0.9 }
115
+ ]
116
+ }
117
+ };
118
+ function successPreset(variant, pitchSemitones) {
119
+ return VARIANTS[variant];
120
+ }
121
+
122
+ // src/sounds/error.ts
123
+ var VARIANTS2 = {
124
+ short: {
125
+ name: "error",
126
+ duration: 0.25,
127
+ notes: [
128
+ {
129
+ frequency: 440,
130
+ duration: 0.1,
131
+ startAt: 0,
132
+ waveShape: "square",
133
+ gain: 0.5,
134
+ envelope: { attack: 5e-3, decay: 0.04, sustain: 0.6, release: 0.06 }
135
+ },
136
+ {
137
+ frequency: 311,
138
+ duration: 0.15,
139
+ startAt: 0.1,
140
+ waveShape: "square",
141
+ gain: 0.6,
142
+ envelope: { attack: 5e-3, decay: 0.04, sustain: 0.6, release: 0.06 }
143
+ }
144
+ ]
145
+ },
146
+ medium: {
147
+ name: "error",
148
+ duration: 0.5,
149
+ notes: [
150
+ {
151
+ frequency: 440,
152
+ duration: 0.12,
153
+ startAt: 0,
154
+ waveShape: "sawtooth",
155
+ gain: 0.4,
156
+ envelope: { attack: 5e-3, decay: 0.06, sustain: 0.5, release: 0.08 }
157
+ },
158
+ {
159
+ frequency: 392,
160
+ duration: 0.12,
161
+ startAt: 0.13,
162
+ waveShape: "sawtooth",
163
+ gain: 0.45,
164
+ envelope: { attack: 5e-3, decay: 0.06, sustain: 0.5, release: 0.08 }
165
+ },
166
+ {
167
+ frequency: 311,
168
+ duration: 0.2,
169
+ startAt: 0.26,
170
+ waveShape: "sawtooth",
171
+ gain: 0.55,
172
+ envelope: { attack: 5e-3, decay: 0.06, sustain: 0.5, release: 0.12 }
173
+ }
174
+ ]
175
+ },
176
+ long: {
177
+ name: "error",
178
+ duration: 0.75,
179
+ notes: [
180
+ {
181
+ frequency: 440,
182
+ duration: 0.12,
183
+ startAt: 0,
184
+ waveShape: "sawtooth",
185
+ gain: 0.4,
186
+ envelope: { attack: 5e-3, decay: 0.07, sustain: 0.5, release: 0.1 }
187
+ },
188
+ {
189
+ frequency: 415,
190
+ duration: 0.1,
191
+ startAt: 0.14,
192
+ waveShape: "sawtooth",
193
+ gain: 0.4,
194
+ envelope: { attack: 5e-3, decay: 0.07, sustain: 0.5, release: 0.1 }
195
+ },
196
+ {
197
+ frequency: 370,
198
+ duration: 0.12,
199
+ startAt: 0.26,
200
+ waveShape: "sawtooth",
201
+ gain: 0.45,
202
+ envelope: { attack: 5e-3, decay: 0.07, sustain: 0.5, release: 0.1 }
203
+ },
204
+ {
205
+ frequency: 311,
206
+ duration: 0.25,
207
+ startAt: 0.4,
208
+ waveShape: "sawtooth",
209
+ gain: 0.55,
210
+ envelope: { attack: 5e-3, decay: 0.08, sustain: 0.5, release: 0.15 }
211
+ }
212
+ ]
213
+ }
214
+ };
215
+ function errorPreset(variant, pitchSemitones) {
216
+ return VARIANTS2[variant];
217
+ }
218
+
219
+ // src/sounds/warning.ts
220
+ var VARIANTS3 = {
221
+ short: {
222
+ name: "warning",
223
+ duration: 0.28,
224
+ notes: [
225
+ { frequency: 587.33, duration: 0.1, startAt: 0, waveShape: "triangle", gain: 0.7 },
226
+ { frequency: 523.25, duration: 0.18, startAt: 0.1, waveShape: "triangle", gain: 0.75 }
227
+ ]
228
+ },
229
+ medium: {
230
+ name: "warning",
231
+ duration: 0.5,
232
+ notes: [
233
+ { frequency: 587.33, duration: 0.12, startAt: 0, waveShape: "triangle", gain: 0.65 },
234
+ { frequency: 587.33, duration: 0.12, startAt: 0.14, waveShape: "triangle", gain: 0.65 },
235
+ { frequency: 493.88, duration: 0.2, startAt: 0.28, waveShape: "triangle", gain: 0.75 }
236
+ ]
237
+ },
238
+ long: {
239
+ name: "warning",
240
+ duration: 0.8,
241
+ notes: [
242
+ { frequency: 587.33, duration: 0.12, startAt: 0, waveShape: "triangle", gain: 0.6 },
243
+ { frequency: 587.33, duration: 0.12, startAt: 0.15, waveShape: "triangle", gain: 0.6 },
244
+ { frequency: 554.37, duration: 0.12, startAt: 0.3, waveShape: "triangle", gain: 0.65 },
245
+ { frequency: 493.88, duration: 0.25, startAt: 0.45, waveShape: "triangle", gain: 0.75 }
246
+ ]
247
+ }
248
+ };
249
+ function warningPreset(variant, pitchSemitones) {
250
+ return VARIANTS3[variant];
251
+ }
252
+
253
+ // src/sounds/notification.ts
254
+ var VARIANTS4 = {
255
+ short: {
256
+ name: "notification",
257
+ duration: 0.22,
258
+ notes: [
259
+ { frequency: 880, duration: 0.08, startAt: 0, waveShape: "sine", gain: 0.6 },
260
+ { frequency: 660, duration: 0.14, startAt: 0.08, waveShape: "sine", gain: 0.65 }
261
+ ]
262
+ },
263
+ medium: {
264
+ name: "notification",
265
+ duration: 0.38,
266
+ notes: [
267
+ { frequency: 880, duration: 0.1, startAt: 0, waveShape: "sine", gain: 0.55 },
268
+ { frequency: 660, duration: 0.28, startAt: 0.1, waveShape: "sine", gain: 0.6 }
269
+ ]
270
+ },
271
+ long: {
272
+ name: "notification",
273
+ duration: 0.55,
274
+ notes: [
275
+ { frequency: 1046.5, duration: 0.08, startAt: 0, waveShape: "sine", gain: 0.5 },
276
+ { frequency: 880, duration: 0.1, startAt: 0.1, waveShape: "sine", gain: 0.55 },
277
+ { frequency: 660, duration: 0.3, startAt: 0.22, waveShape: "sine", gain: 0.6 }
278
+ ]
279
+ }
280
+ };
281
+ function notificationPreset(variant, pitchSemitones) {
282
+ return VARIANTS4[variant];
283
+ }
284
+
285
+ // src/sounds/click.ts
286
+ var VARIANTS5 = {
287
+ short: {
288
+ name: "click",
289
+ duration: 0.04,
290
+ notes: [
291
+ {
292
+ frequency: 1200,
293
+ duration: 0.03,
294
+ startAt: 0,
295
+ waveShape: "sine",
296
+ gain: 0.5,
297
+ envelope: { attack: 2e-3, decay: 0.015, sustain: 0, release: 0.01 }
298
+ }
299
+ ]
300
+ },
301
+ medium: {
302
+ name: "click",
303
+ duration: 0.07,
304
+ notes: [
305
+ {
306
+ frequency: 900,
307
+ duration: 0.05,
308
+ startAt: 0,
309
+ waveShape: "sine",
310
+ gain: 0.55,
311
+ envelope: { attack: 2e-3, decay: 0.03, sustain: 0, release: 0.015 }
312
+ }
313
+ ]
314
+ },
315
+ long: {
316
+ name: "click",
317
+ duration: 0.12,
318
+ notes: [
319
+ {
320
+ frequency: 700,
321
+ duration: 0.08,
322
+ startAt: 0,
323
+ waveShape: "triangle",
324
+ gain: 0.6,
325
+ envelope: { attack: 2e-3, decay: 0.05, sustain: 0, release: 0.025 }
326
+ }
327
+ ]
328
+ }
329
+ };
330
+ function clickPreset(variant, pitchSemitones) {
331
+ return VARIANTS5[variant];
332
+ }
333
+
334
+ // src/sounds/info.ts
335
+ var VARIANTS6 = {
336
+ short: {
337
+ name: "info",
338
+ duration: 0.2,
339
+ notes: [
340
+ {
341
+ frequency: 698.46,
342
+ // F5
343
+ duration: 0.18,
344
+ startAt: 0,
345
+ waveShape: "sine",
346
+ gain: 0.55,
347
+ envelope: { attack: 0.015, decay: 0.04, sustain: 0.5, release: 0.1 }
348
+ }
349
+ ]
350
+ },
351
+ medium: {
352
+ name: "info",
353
+ duration: 0.35,
354
+ notes: [
355
+ {
356
+ frequency: 698.46,
357
+ duration: 0.32,
358
+ startAt: 0,
359
+ waveShape: "sine",
360
+ gain: 0.55,
361
+ envelope: { attack: 0.02, decay: 0.05, sustain: 0.5, release: 0.12 }
362
+ }
363
+ ]
364
+ },
365
+ long: {
366
+ name: "info",
367
+ duration: 0.55,
368
+ notes: [
369
+ {
370
+ frequency: 587.33,
371
+ // D5
372
+ duration: 0.18,
373
+ startAt: 0,
374
+ waveShape: "sine",
375
+ gain: 0.5,
376
+ envelope: { attack: 0.02, decay: 0.05, sustain: 0.4, release: 0.1 }
377
+ },
378
+ {
379
+ frequency: 698.46,
380
+ // F5
381
+ duration: 0.3,
382
+ startAt: 0.2,
383
+ waveShape: "sine",
384
+ gain: 0.55,
385
+ envelope: { attack: 0.02, decay: 0.06, sustain: 0.5, release: 0.12 }
386
+ }
387
+ ]
388
+ }
389
+ };
390
+ function infoPreset(variant, pitchSemitones) {
391
+ return VARIANTS6[variant];
392
+ }
393
+
394
+ // src/index.ts
395
+ async function play(preset, opts = {}) {
396
+ const { volume = 0.5, pitch = 0, audioContext, onEnded } = opts;
397
+ const ctx = audioContext ?? await getAudioContext();
398
+ playSound(ctx, preset, volume, pitch, onEnded);
399
+ }
400
+ async function playSuccess(opts = {}) {
401
+ return play(successPreset(opts.variant ?? "medium", opts.pitch ?? 0), opts);
402
+ }
403
+ async function playError(opts = {}) {
404
+ return play(errorPreset(opts.variant ?? "medium", opts.pitch ?? 0), opts);
405
+ }
406
+ async function playWarning(opts = {}) {
407
+ return play(warningPreset(opts.variant ?? "medium", opts.pitch ?? 0), opts);
408
+ }
409
+ async function playNotification(opts = {}) {
410
+ return play(
411
+ notificationPreset(opts.variant ?? "medium", opts.pitch ?? 0),
412
+ opts
413
+ );
414
+ }
415
+ async function playClick(opts = {}) {
416
+ return play(clickPreset(opts.variant ?? "short", opts.pitch ?? 0), opts);
417
+ }
418
+ async function playInfo(opts = {}) {
419
+ return play(infoPreset(opts.variant ?? "medium", opts.pitch ?? 0), opts);
420
+ }
421
+ var BANK = {
422
+ success: successPreset,
423
+ error: errorPreset,
424
+ warning: warningPreset,
425
+ notification: notificationPreset,
426
+ click: clickPreset,
427
+ info: infoPreset
428
+ };
429
+ async function earcon(name, opts = {}) {
430
+ const preset = BANK[name];
431
+ return play(preset(opts.variant ?? "medium", opts.pitch ?? 0), opts);
432
+ }
433
+ function registerSound(name, preset) {
434
+ BANK[name] = preset;
435
+ }
436
+
437
+ exports.closeAudioContext = closeAudioContext;
438
+ exports.earcon = earcon;
439
+ exports.playClick = playClick;
440
+ exports.playError = playError;
441
+ exports.playInfo = playInfo;
442
+ exports.playNotification = playNotification;
443
+ exports.playSuccess = playSuccess;
444
+ exports.playWarning = playWarning;
445
+ exports.registerSound = registerSound;
446
+ exports.semitonesToMultiplier = semitonesToMultiplier;
447
+ exports.setAudioContext = setAudioContext;
448
+ //# sourceMappingURL=index.cjs.map
449
+ //# sourceMappingURL=index.cjs.map