@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 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
- # 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
+ ## 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