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 +21 -0
- package/README.md +169 -0
- package/dist/earcon.min.global.js +1 -0
- package/dist/index.cjs +449 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +153 -0
- package/dist/index.d.ts +153 -0
- package/dist/index.js +437 -0
- package/dist/index.js.map +1 -0
- package/package.json +65 -0
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
|
+
[](https://www.npmjs.com/package/earcons)
|
|
6
|
+
[](https://bundlephobia.com/package/earcons)
|
|
7
|
+
[](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
|