@jswork/howler-audio-manager 0.0.0-stage → 1.0.2

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 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,258 @@
1
- # Temporary Holding Version
1
+ # howler-audio-manager
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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
+ ## React Hooks
67
+
68
+ 在 React 项目中通过 Provider + hooks 接入(`react` 为可选 peer 依赖,非 React 项目使用说明见文末)。
69
+
70
+ ```shell
71
+ pnpm add @jswork/howler-audio-manager howler react
72
+ ```
73
+
74
+ ```tsx
75
+ import { AudioManagerProvider, useAudio, useAudioEvent, useGlobalVolume, usePreload } from '@jswork/howler-audio-manager';
76
+
77
+ const manager = new AudioManager<SoundEffect>();
78
+ manager.loadEffects(SOUND_CONFIG);
79
+
80
+ export function App() {
81
+ return (
82
+ <AudioManagerProvider manager={manager}>
83
+ <PlayButton />
84
+ </AudioManagerProvider>
85
+ );
86
+ }
87
+
88
+ function PlayButton() {
89
+ // 1. 获取 manager 实例
90
+ const audio = useAudio<SoundEffect>();
91
+
92
+ // 2. 全局音量(双向绑定)
93
+ const [volume, setVolume] = useGlobalVolume();
94
+
95
+ // 3. 订阅事件总线(自动随组件卸载退订,无闭包陷阱)
96
+ useAudioEvent('end', (payload) => {
97
+ console.log('playback ended', payload);
98
+ });
99
+
100
+ // 4. 预加载 URL 音频
101
+ const preload = usePreload(['https://cdn.example.com/voice/lesson-01.mp3']);
102
+
103
+ return (
104
+ <div>
105
+ <button onClick={() => audio.playEffect(SoundEffect.Click)}>click</button>
106
+ <input
107
+ type="range"
108
+ min={0}
109
+ max={1}
110
+ step={0.01}
111
+ value={volume}
112
+ onChange={(e) => setVolume(Number(e.target.value))}
113
+ />
114
+ <p>preload: {preload.status}</p>
115
+ </div>
116
+ );
117
+ }
118
+ ```
119
+
120
+ | API | 签名 | 说明 |
121
+ | --- | --- | --- |
122
+ | `AudioManagerProvider` | `({ manager, children }) => JSX` | Context Provider,向下传递 `AudioManager` 实例;实例生命周期由调用方管理(不随 Provider 卸载销毁)。 |
123
+ | `useAudio` | `<K>() => AudioManager<K>` | 取 Provider 传入的实例;不在 Provider 内调用会抛出明确异常。 |
124
+ | `useAudioEvent` | `<K>(event: AudioEventType, handler: (payload) => void) => void` | 订阅全局事件;handler 永远是最新的(无 stale closure),卸载自动调用 `destroy()`。 |
125
+ | `useGlobalVolume` | `() => [number, (v: number) => void]` | 读取/设置全局音量(初始值来自 manager 的 `defaultVolume`)。 |
126
+ | `usePreload` | `(urls: string[]) => { status: 'idle' \| 'loading' \| 'loaded' \| 'error'; error: unknown }` | 批量预加载 URL 音频,状态随加载结果更新;`urls` 内容变化会重新触发。 |
127
+
128
+ > **非 React 项目注意**:hooks 从主入口导出,`react` 为 optional peerDependency。若你的打包器因找不到 `react` 报错,安装 `react`(任意 `>=16.8` 版本,仅作为打包占位)即可,不会影响现有 `AudioManager` 用法。
129
+
130
+ ## API Reference
131
+
132
+ ### Constructor
133
+
134
+ ```typescript
135
+ new AudioManager<K extends string | number = string>(
136
+ options?: { defaultVolume?: number }, // defaults to 1.0
137
+ inject?: { HowlClass?: typeof Howl; HowlerObj?: typeof Howler } // test/DI hooks
138
+ );
139
+ ```
140
+
141
+ ### Preset Effects
142
+
143
+ | Method | Signature | Description |
144
+ | --- | --- | --- |
145
+ | `loadEffect` | `(key: K, url: string) => void` | Load (or reload) one effect. Replaces an existing instance after `unload()`. Internally `preload: true`. |
146
+ | `loadEffects` | `(map: SoundResourceMap<K>) => void` | Batch load from a `Record<SoundEffect, url>` map. |
147
+ | `hasEffect` | `(key: K) => boolean` | Whether the key is cached. |
148
+ | `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. |
149
+ | `stopEffect` | `(key: K) => void` | Stop a playing effect. |
150
+ | `removeEffect` | `(key: K) => void` | `unload()` the effect and drop it from the cache. |
151
+
152
+ ```typescript
153
+ audioManager.loadEffect(SoundEffect.Click, clickUrl);
154
+ audioManager.playEffect(SoundEffect.Click, {
155
+ volume: 0.8,
156
+ onStart: (id) => console.log('started', id),
157
+ onEnd: (id) => console.log('ended', id)
158
+ });
159
+ audioManager.removeEffect(SoundEffect.Click);
160
+ ```
161
+
162
+ ### Dynamic URL Audio
163
+
164
+ | Method | Signature | Description |
165
+ | --- | --- | --- |
166
+ | `preloadUrl` | `(url: string) => Promise<void>` | Warm up one url in the background; resolves on `load`, rejects on `loaderror`. |
167
+ | `preloadUrls` | `(urls: string[]) => Promise<void[]>` | Parallel warm-up for a batch of urls. |
168
+ | `playUrl` | `(url: string, options?: PlayUrlOptions) => number` | Play on demand (created and cached if missing). |
169
+ | `stopUrl` | `(url: string) => void` | Stop a playing url audio. |
170
+
171
+ `PlayUrlOptions` extends `PlayOptions` with:
172
+
173
+ - `loop?: boolean` — loop playback.
174
+ - `singleton?: boolean` (default `true`) — keep a single active URL channel; set `false` to allow concurrent plays.
175
+
176
+ ```typescript
177
+ // Preload next lesson while the user is on the current one
178
+ await audioManager.preloadUrls([nextUrl1, nextUrl2]);
179
+
180
+ // Singleton voice channel (default)
181
+ audioManager.playUrl(voiceUrl, { loop: false });
182
+
183
+ // Allow concurrent BGM + voice
184
+ audioManager.playUrl(bgmUrl, { loop: true, singleton: false });
185
+ ```
186
+
187
+ > 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.
188
+
189
+ ### Event System
190
+
191
+ ```typescript
192
+ type AudioEventType = 'play' | 'end' | 'stop' | 'pause' | 'error' | 'load';
193
+
194
+ interface AudioEventPayload<K = string> {
195
+ type: 'effect' | 'url';
196
+ key?: K;
197
+ url?: string;
198
+ soundId?: number;
199
+ error?: unknown;
200
+ }
201
+ ```
202
+
203
+ ```typescript
204
+ const subscription = audioManager.on('end', (payload) => {
205
+ if (payload.type === 'url') console.log('url finished', payload.url);
206
+ else console.log('effect finished', payload.key);
207
+ });
208
+
209
+ // One-line teardown — no off(event, fn)
210
+ subscription.destroy();
211
+ ```
212
+
213
+ All global listeners are cleared automatically when `destroy()` is called. Listener exceptions are caught and logged, so one bad handler never breaks playback.
214
+
215
+ ### Global Controls
216
+
217
+ | Method | Signature | Description |
218
+ | --- | --- | --- |
219
+ | `setGlobalVolume` | `(volume: number) => void` | Set global volume (0.0 – 1.0); applied to `Howler` and to future loads. |
220
+ | `mute` | `(muted: boolean) => void` | Globally mute / unmute. |
221
+ | `stopAll` | `() => void` | Stop every playing sound and reset the active URL channel. |
222
+
223
+ ### Lifecycle & Cleanup
224
+
225
+ | Method | Signature | Description |
226
+ | --- | --- | --- |
227
+ | `removeEffect` | `(key: K) => void` | Dispose a single effect instance. |
228
+ | `destroy` | `() => void` | `unload()` every cached `Howl`, clear both caches, drop all listeners, reset the singleton channel — no leaked instances. |
229
+
230
+ ```typescript
231
+ // React
232
+ useEffect(() => {
233
+ const manager = new AudioManager<SoundEffect>();
234
+ manager.loadEffects(SOUND_CONFIG);
235
+ return () => manager.destroy();
236
+ }, []);
237
+ ```
238
+
239
+ ## Development
240
+
241
+ ```shell
242
+ pnpm install
243
+ pnpm run build # tsup -> dist/index.cjs.js, dist/index.esm.js, dist/index.umd.js, dist/index.d.ts
244
+ bun test # unit tests
245
+ ```
246
+
247
+ ## License
248
+
249
+ Code released under [the MIT license](https://github.com/afeiship/howler-audio-manager/blob/main/LICENSE.txt).
250
+
251
+ [version-image]: https://img.shields.io/npm/v/@jswork/howler-audio-manager
252
+ [version-url]: https://npmjs.org/package/@jswork/howler-audio-manager
253
+
254
+ [license-image]: https://img.shields.io/npm/l/@jswork/howler-audio-manager
255
+ [license-url]: https://github.com/afeiship/howler-audio-manager/blob/main/LICENSE.txt
256
+
257
+ [download-image]: https://img.shields.io/npm/dm/@jswork/howler-audio-manager
258
+ [download-url]: https://www.npmjs.com/package/@jswork/howler-audio-manager