@jswork/howler-audio-manager 0.0.0-stage → 1.0.1
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.txt +21 -0
- package/README.md +193 -2
- package/dist/index.cjs.js +612 -0
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.d.mts +70 -0
- package/dist/index.d.ts +72 -0
- package/dist/index.esm.js +403 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.umd.js +485 -0
- package/dist/index.umd.js.map +1 -0
- package/package.json +51 -4
- package/src/index.ts +3 -0
- package/src/manager.ts +313 -0
- package/src/types.ts +40 -0
package/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2019 afei <1290657123@qq.com>
|
|
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
|
|
13
|
+
all 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
|
|
21
|
+
THE SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,194 @@
|
|
|
1
|
-
#
|
|
1
|
+
# howler-audio-manager
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> H5/Web audio management powered by howler.js — enum-driven preset sounds, dual caching (effect cache + URL audio cache), singleton dynamic URL playback, and zero memory leak on destroy.
|
|
4
|
+
|
|
5
|
+
[![version][version-image]][version-url]
|
|
6
|
+
[![license][license-image]][license-url]
|
|
7
|
+
[![download][download-image]][download-url]
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **Enum-driven**: TypeScript generic `AudioManager<K>` accepts your own sound enum / union type — no hardcoded business keys.
|
|
12
|
+
- **Dual caching**: independent `effectCache` (keyed by enum) and `urlAudioCache` (keyed by full url).
|
|
13
|
+
- **Singleton URL audio**: dynamic audio (voice/TTS/BGM) keeps a single active channel by default; new playback stops the previous one (opt out with `singleton: false`).
|
|
14
|
+
- **Overlap prevention**: replaying the same key/url always stops the previous play first.
|
|
15
|
+
- **Preload strategy**: effects preload eagerly (`preload: true`) for zero-latency feedback; URLs preload on demand or via `preloadUrl(s)`.
|
|
16
|
+
- **Zero memory leak**: `destroy()` unloads every `Howl`, clears both caches and all event listeners — safe for SPA route unmounts.
|
|
17
|
+
- **Dual-layer events**: per-call callbacks (`onStart`/`onEnd`/`onError`/`onStop`) plus a global event bus whose subscription returns `{ destroy }` — no `off()` needed.
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```shell
|
|
22
|
+
pnpm add @jswork/howler-audio-manager howler
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`howler` is a peer dependency.
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import { AudioManager } from '@jswork/howler-audio-manager';
|
|
31
|
+
|
|
32
|
+
// 1. Business-side sound enum
|
|
33
|
+
export enum SoundEffect {
|
|
34
|
+
Click = 'CLICK',
|
|
35
|
+
Success = 'SUCCESS',
|
|
36
|
+
Fail = 'FAIL',
|
|
37
|
+
Reward = 'REWARD'
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 2. Resource map
|
|
41
|
+
const SOUND_CONFIG: Record<SoundEffect, string> = {
|
|
42
|
+
[SoundEffect.Click]: 'https://cdn.example.com/audio/click.mp3',
|
|
43
|
+
[SoundEffect.Success]: 'https://cdn.example.com/audio/success.mp3',
|
|
44
|
+
[SoundEffect.Fail]: 'https://cdn.example.com/audio/fail.mp3',
|
|
45
|
+
[SoundEffect.Reward]: 'https://cdn.example.com/audio/reward.mp3'
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// 3. Instantiate with full type constraints
|
|
49
|
+
const audioManager = new AudioManager<SoundEffect>();
|
|
50
|
+
audioManager.loadEffects(SOUND_CONFIG);
|
|
51
|
+
|
|
52
|
+
// 4. Play a preset effect
|
|
53
|
+
audioManager.playEffect(SoundEffect.Success);
|
|
54
|
+
|
|
55
|
+
// 5. Play dynamic URL audio (voice / lesson reading / TTS)
|
|
56
|
+
audioManager.playUrl('https://cdn.example.com/voice/lesson-01.mp3', {
|
|
57
|
+
onStart: () => console.log('playback started'),
|
|
58
|
+
onEnd: () => console.log('playback finished'),
|
|
59
|
+
onError: (_, err) => console.error('playback failed', err)
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// 6. Clean up on page/component unmount
|
|
63
|
+
audioManager.destroy();
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## API Reference
|
|
67
|
+
|
|
68
|
+
### Constructor
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
new AudioManager<K extends string | number = string>(
|
|
72
|
+
options?: { defaultVolume?: number }, // defaults to 1.0
|
|
73
|
+
inject?: { HowlClass?: typeof Howl; HowlerObj?: typeof Howler } // test/DI hooks
|
|
74
|
+
);
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Preset Effects
|
|
78
|
+
|
|
79
|
+
| Method | Signature | Description |
|
|
80
|
+
| --- | --- | --- |
|
|
81
|
+
| `loadEffect` | `(key: K, url: string) => void` | Load (or reload) one effect. Replaces an existing instance after `unload()`. Internally `preload: true`. |
|
|
82
|
+
| `loadEffects` | `(map: SoundResourceMap<K>) => void` | Batch load from a `Record<SoundEffect, url>` map. |
|
|
83
|
+
| `hasEffect` | `(key: K) => boolean` | Whether the key is cached. |
|
|
84
|
+
| `playEffect` | `(key: K, options?: PlayOptions) => number \| undefined` | Play a cached effect; stops a previous play of the same key first. Warns via `console.warn` and returns `undefined` when the key is not loaded. |
|
|
85
|
+
| `stopEffect` | `(key: K) => void` | Stop a playing effect. |
|
|
86
|
+
| `removeEffect` | `(key: K) => void` | `unload()` the effect and drop it from the cache. |
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
audioManager.loadEffect(SoundEffect.Click, clickUrl);
|
|
90
|
+
audioManager.playEffect(SoundEffect.Click, {
|
|
91
|
+
volume: 0.8,
|
|
92
|
+
onStart: (id) => console.log('started', id),
|
|
93
|
+
onEnd: (id) => console.log('ended', id)
|
|
94
|
+
});
|
|
95
|
+
audioManager.removeEffect(SoundEffect.Click);
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Dynamic URL Audio
|
|
99
|
+
|
|
100
|
+
| Method | Signature | Description |
|
|
101
|
+
| --- | --- | --- |
|
|
102
|
+
| `preloadUrl` | `(url: string) => Promise<void>` | Warm up one url in the background; resolves on `load`, rejects on `loaderror`. |
|
|
103
|
+
| `preloadUrls` | `(urls: string[]) => Promise<void[]>` | Parallel warm-up for a batch of urls. |
|
|
104
|
+
| `playUrl` | `(url: string, options?: PlayUrlOptions) => number` | Play on demand (created and cached if missing). |
|
|
105
|
+
| `stopUrl` | `(url: string) => void` | Stop a playing url audio. |
|
|
106
|
+
|
|
107
|
+
`PlayUrlOptions` extends `PlayOptions` with:
|
|
108
|
+
|
|
109
|
+
- `loop?: boolean` — loop playback.
|
|
110
|
+
- `singleton?: boolean` (default `true`) — keep a single active URL channel; set `false` to allow concurrent plays.
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
// Preload next lesson while the user is on the current one
|
|
114
|
+
await audioManager.preloadUrls([nextUrl1, nextUrl2]);
|
|
115
|
+
|
|
116
|
+
// Singleton voice channel (default)
|
|
117
|
+
audioManager.playUrl(voiceUrl, { loop: false });
|
|
118
|
+
|
|
119
|
+
// Allow concurrent BGM + voice
|
|
120
|
+
audioManager.playUrl(bgmUrl, { loop: true, singleton: false });
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
> Mobile autoplay policy: iOS / WeChat webviews require a user gesture for the first sound output. Preloading only caches network resources and decodes instances — it never bypasses the browser policy.
|
|
124
|
+
|
|
125
|
+
### Event System
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
type AudioEventType = 'play' | 'end' | 'stop' | 'pause' | 'error' | 'load';
|
|
129
|
+
|
|
130
|
+
interface AudioEventPayload<K = string> {
|
|
131
|
+
type: 'effect' | 'url';
|
|
132
|
+
key?: K;
|
|
133
|
+
url?: string;
|
|
134
|
+
soundId?: number;
|
|
135
|
+
error?: unknown;
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
const subscription = audioManager.on('end', (payload) => {
|
|
141
|
+
if (payload.type === 'url') console.log('url finished', payload.url);
|
|
142
|
+
else console.log('effect finished', payload.key);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// One-line teardown — no off(event, fn)
|
|
146
|
+
subscription.destroy();
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
All global listeners are cleared automatically when `destroy()` is called. Listener exceptions are caught and logged, so one bad handler never breaks playback.
|
|
150
|
+
|
|
151
|
+
### Global Controls
|
|
152
|
+
|
|
153
|
+
| Method | Signature | Description |
|
|
154
|
+
| --- | --- | --- |
|
|
155
|
+
| `setGlobalVolume` | `(volume: number) => void` | Set global volume (0.0 – 1.0); applied to `Howler` and to future loads. |
|
|
156
|
+
| `mute` | `(muted: boolean) => void` | Globally mute / unmute. |
|
|
157
|
+
| `stopAll` | `() => void` | Stop every playing sound and reset the active URL channel. |
|
|
158
|
+
|
|
159
|
+
### Lifecycle & Cleanup
|
|
160
|
+
|
|
161
|
+
| Method | Signature | Description |
|
|
162
|
+
| --- | --- | --- |
|
|
163
|
+
| `removeEffect` | `(key: K) => void` | Dispose a single effect instance. |
|
|
164
|
+
| `destroy` | `() => void` | `unload()` every cached `Howl`, clear both caches, drop all listeners, reset the singleton channel — no leaked instances. |
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
// React
|
|
168
|
+
useEffect(() => {
|
|
169
|
+
const manager = new AudioManager<SoundEffect>();
|
|
170
|
+
manager.loadEffects(SOUND_CONFIG);
|
|
171
|
+
return () => manager.destroy();
|
|
172
|
+
}, []);
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Development
|
|
176
|
+
|
|
177
|
+
```shell
|
|
178
|
+
pnpm install
|
|
179
|
+
pnpm run build # tsup -> dist/index.cjs.js, dist/index.esm.js, dist/index.umd.js, dist/index.d.ts
|
|
180
|
+
bun test # unit tests
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## License
|
|
184
|
+
|
|
185
|
+
Code released under [the MIT license](https://github.com/afeiship/howler-audio-manager/blob/main/LICENSE.txt).
|
|
186
|
+
|
|
187
|
+
[version-image]: https://img.shields.io/npm/v/@jswork/howler-audio-manager
|
|
188
|
+
[version-url]: https://npmjs.org/package/@jswork/howler-audio-manager
|
|
189
|
+
|
|
190
|
+
[license-image]: https://img.shields.io/npm/l/@jswork/howler-audio-manager
|
|
191
|
+
[license-url]: https://github.com/afeiship/howler-audio-manager/blob/main/LICENSE.txt
|
|
192
|
+
|
|
193
|
+
[download-image]: https://img.shields.io/npm/dm/@jswork/howler-audio-manager
|
|
194
|
+
[download-url]: https://www.npmjs.com/package/@jswork/howler-audio-manager
|