star-audio 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/dist/index.js ADDED
@@ -0,0 +1,3149 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __commonJS = (cb, mod) => function __require() {
9
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
10
+ };
11
+ var __export = (target, all) => {
12
+ for (var name in all)
13
+ __defProp(target, name, { get: all[name], enumerable: true });
14
+ };
15
+ var __copyProps = (to, from, except, desc) => {
16
+ if (from && typeof from === "object" || typeof from === "function") {
17
+ for (let key of __getOwnPropNames(from))
18
+ if (!__hasOwnProp.call(to, key) && key !== except)
19
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
20
+ }
21
+ return to;
22
+ };
23
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
24
+ // If the importer is in node compatibility mode or this is not an ESM
25
+ // file that has been converted to a CommonJS file using a Babel-
26
+ // compatible transform (i.e. "__esModule" has not been set), then set
27
+ // "default" to the CommonJS "module.exports" for node compatibility.
28
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
29
+ mod
30
+ ));
31
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
+
33
+ // ../../node_modules/howler/dist/howler.js
34
+ var require_howler = __commonJS({
35
+ "../../node_modules/howler/dist/howler.js"(exports2) {
36
+ "use strict";
37
+ (function() {
38
+ "use strict";
39
+ var HowlerGlobal2 = function() {
40
+ this.init();
41
+ };
42
+ HowlerGlobal2.prototype = {
43
+ /**
44
+ * Initialize the global Howler object.
45
+ * @return {Howler}
46
+ */
47
+ init: function() {
48
+ var self = this || Howler3;
49
+ self._counter = 1e3;
50
+ self._html5AudioPool = [];
51
+ self.html5PoolSize = 10;
52
+ self._codecs = {};
53
+ self._howls = [];
54
+ self._muted = false;
55
+ self._volume = 1;
56
+ self._canPlayEvent = "canplaythrough";
57
+ self._navigator = typeof window !== "undefined" && window.navigator ? window.navigator : null;
58
+ self.masterGain = null;
59
+ self.noAudio = false;
60
+ self.usingWebAudio = true;
61
+ self.autoSuspend = true;
62
+ self.ctx = null;
63
+ self.autoUnlock = true;
64
+ self._setup();
65
+ return self;
66
+ },
67
+ /**
68
+ * Get/set the global volume for all sounds.
69
+ * @param {Float} vol Volume from 0.0 to 1.0.
70
+ * @return {Howler/Float} Returns self or current volume.
71
+ */
72
+ volume: function(vol) {
73
+ var self = this || Howler3;
74
+ vol = parseFloat(vol);
75
+ if (!self.ctx) {
76
+ setupAudioContext();
77
+ }
78
+ if (typeof vol !== "undefined" && vol >= 0 && vol <= 1) {
79
+ self._volume = vol;
80
+ if (self._muted) {
81
+ return self;
82
+ }
83
+ if (self.usingWebAudio) {
84
+ self.masterGain.gain.setValueAtTime(vol, Howler3.ctx.currentTime);
85
+ }
86
+ for (var i = 0; i < self._howls.length; i++) {
87
+ if (!self._howls[i]._webAudio) {
88
+ var ids = self._howls[i]._getSoundIds();
89
+ for (var j = 0; j < ids.length; j++) {
90
+ var sound = self._howls[i]._soundById(ids[j]);
91
+ if (sound && sound._node) {
92
+ sound._node.volume = sound._volume * vol;
93
+ }
94
+ }
95
+ }
96
+ }
97
+ return self;
98
+ }
99
+ return self._volume;
100
+ },
101
+ /**
102
+ * Handle muting and unmuting globally.
103
+ * @param {Boolean} muted Is muted or not.
104
+ */
105
+ mute: function(muted) {
106
+ var self = this || Howler3;
107
+ if (!self.ctx) {
108
+ setupAudioContext();
109
+ }
110
+ self._muted = muted;
111
+ if (self.usingWebAudio) {
112
+ self.masterGain.gain.setValueAtTime(muted ? 0 : self._volume, Howler3.ctx.currentTime);
113
+ }
114
+ for (var i = 0; i < self._howls.length; i++) {
115
+ if (!self._howls[i]._webAudio) {
116
+ var ids = self._howls[i]._getSoundIds();
117
+ for (var j = 0; j < ids.length; j++) {
118
+ var sound = self._howls[i]._soundById(ids[j]);
119
+ if (sound && sound._node) {
120
+ sound._node.muted = muted ? true : sound._muted;
121
+ }
122
+ }
123
+ }
124
+ }
125
+ return self;
126
+ },
127
+ /**
128
+ * Handle stopping all sounds globally.
129
+ */
130
+ stop: function() {
131
+ var self = this || Howler3;
132
+ for (var i = 0; i < self._howls.length; i++) {
133
+ self._howls[i].stop();
134
+ }
135
+ return self;
136
+ },
137
+ /**
138
+ * Unload and destroy all currently loaded Howl objects.
139
+ * @return {Howler}
140
+ */
141
+ unload: function() {
142
+ var self = this || Howler3;
143
+ for (var i = self._howls.length - 1; i >= 0; i--) {
144
+ self._howls[i].unload();
145
+ }
146
+ if (self.usingWebAudio && self.ctx && typeof self.ctx.close !== "undefined") {
147
+ self.ctx.close();
148
+ self.ctx = null;
149
+ setupAudioContext();
150
+ }
151
+ return self;
152
+ },
153
+ /**
154
+ * Check for codec support of specific extension.
155
+ * @param {String} ext Audio file extention.
156
+ * @return {Boolean}
157
+ */
158
+ codecs: function(ext) {
159
+ return (this || Howler3)._codecs[ext.replace(/^x-/, "")];
160
+ },
161
+ /**
162
+ * Setup various state values for global tracking.
163
+ * @return {Howler}
164
+ */
165
+ _setup: function() {
166
+ var self = this || Howler3;
167
+ self.state = self.ctx ? self.ctx.state || "suspended" : "suspended";
168
+ self._autoSuspend();
169
+ if (!self.usingWebAudio) {
170
+ if (typeof Audio !== "undefined") {
171
+ try {
172
+ var test = new Audio();
173
+ if (typeof test.oncanplaythrough === "undefined") {
174
+ self._canPlayEvent = "canplay";
175
+ }
176
+ } catch (e) {
177
+ self.noAudio = true;
178
+ }
179
+ } else {
180
+ self.noAudio = true;
181
+ }
182
+ }
183
+ try {
184
+ var test = new Audio();
185
+ if (test.muted) {
186
+ self.noAudio = true;
187
+ }
188
+ } catch (e) {
189
+ }
190
+ if (!self.noAudio) {
191
+ self._setupCodecs();
192
+ }
193
+ return self;
194
+ },
195
+ /**
196
+ * Check for browser support for various codecs and cache the results.
197
+ * @return {Howler}
198
+ */
199
+ _setupCodecs: function() {
200
+ var self = this || Howler3;
201
+ var audioTest = null;
202
+ try {
203
+ audioTest = typeof Audio !== "undefined" ? new Audio() : null;
204
+ } catch (err) {
205
+ return self;
206
+ }
207
+ if (!audioTest || typeof audioTest.canPlayType !== "function") {
208
+ return self;
209
+ }
210
+ var mpegTest = audioTest.canPlayType("audio/mpeg;").replace(/^no$/, "");
211
+ var ua = self._navigator ? self._navigator.userAgent : "";
212
+ var checkOpera = ua.match(/OPR\/(\d+)/g);
213
+ var isOldOpera = checkOpera && parseInt(checkOpera[0].split("/")[1], 10) < 33;
214
+ var checkSafari = ua.indexOf("Safari") !== -1 && ua.indexOf("Chrome") === -1;
215
+ var safariVersion = ua.match(/Version\/(.*?) /);
216
+ var isOldSafari = checkSafari && safariVersion && parseInt(safariVersion[1], 10) < 15;
217
+ self._codecs = {
218
+ mp3: !!(!isOldOpera && (mpegTest || audioTest.canPlayType("audio/mp3;").replace(/^no$/, ""))),
219
+ mpeg: !!mpegTest,
220
+ opus: !!audioTest.canPlayType('audio/ogg; codecs="opus"').replace(/^no$/, ""),
221
+ ogg: !!audioTest.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/, ""),
222
+ oga: !!audioTest.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/, ""),
223
+ wav: !!(audioTest.canPlayType('audio/wav; codecs="1"') || audioTest.canPlayType("audio/wav")).replace(/^no$/, ""),
224
+ aac: !!audioTest.canPlayType("audio/aac;").replace(/^no$/, ""),
225
+ caf: !!audioTest.canPlayType("audio/x-caf;").replace(/^no$/, ""),
226
+ m4a: !!(audioTest.canPlayType("audio/x-m4a;") || audioTest.canPlayType("audio/m4a;") || audioTest.canPlayType("audio/aac;")).replace(/^no$/, ""),
227
+ m4b: !!(audioTest.canPlayType("audio/x-m4b;") || audioTest.canPlayType("audio/m4b;") || audioTest.canPlayType("audio/aac;")).replace(/^no$/, ""),
228
+ mp4: !!(audioTest.canPlayType("audio/x-mp4;") || audioTest.canPlayType("audio/mp4;") || audioTest.canPlayType("audio/aac;")).replace(/^no$/, ""),
229
+ weba: !!(!isOldSafari && audioTest.canPlayType('audio/webm; codecs="vorbis"').replace(/^no$/, "")),
230
+ webm: !!(!isOldSafari && audioTest.canPlayType('audio/webm; codecs="vorbis"').replace(/^no$/, "")),
231
+ dolby: !!audioTest.canPlayType('audio/mp4; codecs="ec-3"').replace(/^no$/, ""),
232
+ flac: !!(audioTest.canPlayType("audio/x-flac;") || audioTest.canPlayType("audio/flac;")).replace(/^no$/, "")
233
+ };
234
+ return self;
235
+ },
236
+ /**
237
+ * Some browsers/devices will only allow audio to be played after a user interaction.
238
+ * Attempt to automatically unlock audio on the first user interaction.
239
+ * Concept from: http://paulbakaus.com/tutorials/html5/web-audio-on-ios/
240
+ * @return {Howler}
241
+ */
242
+ _unlockAudio: function() {
243
+ var self = this || Howler3;
244
+ if (self._audioUnlocked || !self.ctx) {
245
+ return;
246
+ }
247
+ self._audioUnlocked = false;
248
+ self.autoUnlock = false;
249
+ if (!self._mobileUnloaded && self.ctx.sampleRate !== 44100) {
250
+ self._mobileUnloaded = true;
251
+ self.unload();
252
+ }
253
+ self._scratchBuffer = self.ctx.createBuffer(1, 1, 22050);
254
+ var unlock = function(e) {
255
+ while (self._html5AudioPool.length < self.html5PoolSize) {
256
+ try {
257
+ var audioNode = new Audio();
258
+ audioNode._unlocked = true;
259
+ self._releaseHtml5Audio(audioNode);
260
+ } catch (e2) {
261
+ self.noAudio = true;
262
+ break;
263
+ }
264
+ }
265
+ for (var i = 0; i < self._howls.length; i++) {
266
+ if (!self._howls[i]._webAudio) {
267
+ var ids = self._howls[i]._getSoundIds();
268
+ for (var j = 0; j < ids.length; j++) {
269
+ var sound = self._howls[i]._soundById(ids[j]);
270
+ if (sound && sound._node && !sound._node._unlocked) {
271
+ sound._node._unlocked = true;
272
+ sound._node.load();
273
+ }
274
+ }
275
+ }
276
+ }
277
+ self._autoResume();
278
+ var source = self.ctx.createBufferSource();
279
+ source.buffer = self._scratchBuffer;
280
+ source.connect(self.ctx.destination);
281
+ if (typeof source.start === "undefined") {
282
+ source.noteOn(0);
283
+ } else {
284
+ source.start(0);
285
+ }
286
+ if (typeof self.ctx.resume === "function") {
287
+ self.ctx.resume();
288
+ }
289
+ source.onended = function() {
290
+ source.disconnect(0);
291
+ self._audioUnlocked = true;
292
+ document.removeEventListener("touchstart", unlock, true);
293
+ document.removeEventListener("touchend", unlock, true);
294
+ document.removeEventListener("click", unlock, true);
295
+ document.removeEventListener("keydown", unlock, true);
296
+ for (var i2 = 0; i2 < self._howls.length; i2++) {
297
+ self._howls[i2]._emit("unlock");
298
+ }
299
+ };
300
+ };
301
+ document.addEventListener("touchstart", unlock, true);
302
+ document.addEventListener("touchend", unlock, true);
303
+ document.addEventListener("click", unlock, true);
304
+ document.addEventListener("keydown", unlock, true);
305
+ return self;
306
+ },
307
+ /**
308
+ * Get an unlocked HTML5 Audio object from the pool. If none are left,
309
+ * return a new Audio object and throw a warning.
310
+ * @return {Audio} HTML5 Audio object.
311
+ */
312
+ _obtainHtml5Audio: function() {
313
+ var self = this || Howler3;
314
+ if (self._html5AudioPool.length) {
315
+ return self._html5AudioPool.pop();
316
+ }
317
+ var testPlay = new Audio().play();
318
+ if (testPlay && typeof Promise !== "undefined" && (testPlay instanceof Promise || typeof testPlay.then === "function")) {
319
+ testPlay.catch(function() {
320
+ console.warn("HTML5 Audio pool exhausted, returning potentially locked audio object.");
321
+ });
322
+ }
323
+ return new Audio();
324
+ },
325
+ /**
326
+ * Return an activated HTML5 Audio object to the pool.
327
+ * @return {Howler}
328
+ */
329
+ _releaseHtml5Audio: function(audio) {
330
+ var self = this || Howler3;
331
+ if (audio._unlocked) {
332
+ self._html5AudioPool.push(audio);
333
+ }
334
+ return self;
335
+ },
336
+ /**
337
+ * Automatically suspend the Web Audio AudioContext after no sound has played for 30 seconds.
338
+ * This saves processing/energy and fixes various browser-specific bugs with audio getting stuck.
339
+ * @return {Howler}
340
+ */
341
+ _autoSuspend: function() {
342
+ var self = this;
343
+ if (!self.autoSuspend || !self.ctx || typeof self.ctx.suspend === "undefined" || !Howler3.usingWebAudio) {
344
+ return;
345
+ }
346
+ for (var i = 0; i < self._howls.length; i++) {
347
+ if (self._howls[i]._webAudio) {
348
+ for (var j = 0; j < self._howls[i]._sounds.length; j++) {
349
+ if (!self._howls[i]._sounds[j]._paused) {
350
+ return self;
351
+ }
352
+ }
353
+ }
354
+ }
355
+ if (self._suspendTimer) {
356
+ clearTimeout(self._suspendTimer);
357
+ }
358
+ self._suspendTimer = setTimeout(function() {
359
+ if (!self.autoSuspend) {
360
+ return;
361
+ }
362
+ self._suspendTimer = null;
363
+ self.state = "suspending";
364
+ var handleSuspension = function() {
365
+ self.state = "suspended";
366
+ if (self._resumeAfterSuspend) {
367
+ delete self._resumeAfterSuspend;
368
+ self._autoResume();
369
+ }
370
+ };
371
+ self.ctx.suspend().then(handleSuspension, handleSuspension);
372
+ }, 3e4);
373
+ return self;
374
+ },
375
+ /**
376
+ * Automatically resume the Web Audio AudioContext when a new sound is played.
377
+ * @return {Howler}
378
+ */
379
+ _autoResume: function() {
380
+ var self = this;
381
+ if (!self.ctx || typeof self.ctx.resume === "undefined" || !Howler3.usingWebAudio) {
382
+ return;
383
+ }
384
+ if (self.state === "running" && self.ctx.state !== "interrupted" && self._suspendTimer) {
385
+ clearTimeout(self._suspendTimer);
386
+ self._suspendTimer = null;
387
+ } else if (self.state === "suspended" || self.state === "running" && self.ctx.state === "interrupted") {
388
+ self.ctx.resume().then(function() {
389
+ self.state = "running";
390
+ for (var i = 0; i < self._howls.length; i++) {
391
+ self._howls[i]._emit("resume");
392
+ }
393
+ });
394
+ if (self._suspendTimer) {
395
+ clearTimeout(self._suspendTimer);
396
+ self._suspendTimer = null;
397
+ }
398
+ } else if (self.state === "suspending") {
399
+ self._resumeAfterSuspend = true;
400
+ }
401
+ return self;
402
+ }
403
+ };
404
+ var Howler3 = new HowlerGlobal2();
405
+ var Howl3 = function(o) {
406
+ var self = this;
407
+ if (!o.src || o.src.length === 0) {
408
+ console.error("An array of source files must be passed with any new Howl.");
409
+ return;
410
+ }
411
+ self.init(o);
412
+ };
413
+ Howl3.prototype = {
414
+ /**
415
+ * Initialize a new Howl group object.
416
+ * @param {Object} o Passed in properties for this group.
417
+ * @return {Howl}
418
+ */
419
+ init: function(o) {
420
+ var self = this;
421
+ if (!Howler3.ctx) {
422
+ setupAudioContext();
423
+ }
424
+ self._autoplay = o.autoplay || false;
425
+ self._format = typeof o.format !== "string" ? o.format : [o.format];
426
+ self._html5 = o.html5 || false;
427
+ self._muted = o.mute || false;
428
+ self._loop = o.loop || false;
429
+ self._pool = o.pool || 5;
430
+ self._preload = typeof o.preload === "boolean" || o.preload === "metadata" ? o.preload : true;
431
+ self._rate = o.rate || 1;
432
+ self._sprite = o.sprite || {};
433
+ self._src = typeof o.src !== "string" ? o.src : [o.src];
434
+ self._volume = o.volume !== void 0 ? o.volume : 1;
435
+ self._xhr = {
436
+ method: o.xhr && o.xhr.method ? o.xhr.method : "GET",
437
+ headers: o.xhr && o.xhr.headers ? o.xhr.headers : null,
438
+ withCredentials: o.xhr && o.xhr.withCredentials ? o.xhr.withCredentials : false
439
+ };
440
+ self._duration = 0;
441
+ self._state = "unloaded";
442
+ self._sounds = [];
443
+ self._endTimers = {};
444
+ self._queue = [];
445
+ self._playLock = false;
446
+ self._onend = o.onend ? [{ fn: o.onend }] : [];
447
+ self._onfade = o.onfade ? [{ fn: o.onfade }] : [];
448
+ self._onload = o.onload ? [{ fn: o.onload }] : [];
449
+ self._onloaderror = o.onloaderror ? [{ fn: o.onloaderror }] : [];
450
+ self._onplayerror = o.onplayerror ? [{ fn: o.onplayerror }] : [];
451
+ self._onpause = o.onpause ? [{ fn: o.onpause }] : [];
452
+ self._onplay = o.onplay ? [{ fn: o.onplay }] : [];
453
+ self._onstop = o.onstop ? [{ fn: o.onstop }] : [];
454
+ self._onmute = o.onmute ? [{ fn: o.onmute }] : [];
455
+ self._onvolume = o.onvolume ? [{ fn: o.onvolume }] : [];
456
+ self._onrate = o.onrate ? [{ fn: o.onrate }] : [];
457
+ self._onseek = o.onseek ? [{ fn: o.onseek }] : [];
458
+ self._onunlock = o.onunlock ? [{ fn: o.onunlock }] : [];
459
+ self._onresume = [];
460
+ self._webAudio = Howler3.usingWebAudio && !self._html5;
461
+ if (typeof Howler3.ctx !== "undefined" && Howler3.ctx && Howler3.autoUnlock) {
462
+ Howler3._unlockAudio();
463
+ }
464
+ Howler3._howls.push(self);
465
+ if (self._autoplay) {
466
+ self._queue.push({
467
+ event: "play",
468
+ action: function() {
469
+ self.play();
470
+ }
471
+ });
472
+ }
473
+ if (self._preload && self._preload !== "none") {
474
+ self.load();
475
+ }
476
+ return self;
477
+ },
478
+ /**
479
+ * Load the audio file.
480
+ * @return {Howler}
481
+ */
482
+ load: function() {
483
+ var self = this;
484
+ var url = null;
485
+ if (Howler3.noAudio) {
486
+ self._emit("loaderror", null, "No audio support.");
487
+ return;
488
+ }
489
+ if (typeof self._src === "string") {
490
+ self._src = [self._src];
491
+ }
492
+ for (var i = 0; i < self._src.length; i++) {
493
+ var ext, str;
494
+ if (self._format && self._format[i]) {
495
+ ext = self._format[i];
496
+ } else {
497
+ str = self._src[i];
498
+ if (typeof str !== "string") {
499
+ self._emit("loaderror", null, "Non-string found in selected audio sources - ignoring.");
500
+ continue;
501
+ }
502
+ ext = /^data:audio\/([^;,]+);/i.exec(str);
503
+ if (!ext) {
504
+ ext = /\.([^.]+)$/.exec(str.split("?", 1)[0]);
505
+ }
506
+ if (ext) {
507
+ ext = ext[1].toLowerCase();
508
+ }
509
+ }
510
+ if (!ext) {
511
+ console.warn('No file extension was found. Consider using the "format" property or specify an extension.');
512
+ }
513
+ if (ext && Howler3.codecs(ext)) {
514
+ url = self._src[i];
515
+ break;
516
+ }
517
+ }
518
+ if (!url) {
519
+ self._emit("loaderror", null, "No codec support for selected audio sources.");
520
+ return;
521
+ }
522
+ self._src = url;
523
+ self._state = "loading";
524
+ if (window.location.protocol === "https:" && url.slice(0, 5) === "http:") {
525
+ self._html5 = true;
526
+ self._webAudio = false;
527
+ }
528
+ new Sound2(self);
529
+ if (self._webAudio) {
530
+ loadBuffer(self);
531
+ }
532
+ return self;
533
+ },
534
+ /**
535
+ * Play a sound or resume previous playback.
536
+ * @param {String/Number} sprite Sprite name for sprite playback or sound id to continue previous.
537
+ * @param {Boolean} internal Internal Use: true prevents event firing.
538
+ * @return {Number} Sound ID.
539
+ */
540
+ play: function(sprite, internal) {
541
+ var self = this;
542
+ var id = null;
543
+ if (typeof sprite === "number") {
544
+ id = sprite;
545
+ sprite = null;
546
+ } else if (typeof sprite === "string" && self._state === "loaded" && !self._sprite[sprite]) {
547
+ return null;
548
+ } else if (typeof sprite === "undefined") {
549
+ sprite = "__default";
550
+ if (!self._playLock) {
551
+ var num = 0;
552
+ for (var i = 0; i < self._sounds.length; i++) {
553
+ if (self._sounds[i]._paused && !self._sounds[i]._ended) {
554
+ num++;
555
+ id = self._sounds[i]._id;
556
+ }
557
+ }
558
+ if (num === 1) {
559
+ sprite = null;
560
+ } else {
561
+ id = null;
562
+ }
563
+ }
564
+ }
565
+ var sound = id ? self._soundById(id) : self._inactiveSound();
566
+ if (!sound) {
567
+ return null;
568
+ }
569
+ if (id && !sprite) {
570
+ sprite = sound._sprite || "__default";
571
+ }
572
+ if (self._state !== "loaded") {
573
+ sound._sprite = sprite;
574
+ sound._ended = false;
575
+ var soundId = sound._id;
576
+ self._queue.push({
577
+ event: "play",
578
+ action: function() {
579
+ self.play(soundId);
580
+ }
581
+ });
582
+ return soundId;
583
+ }
584
+ if (id && !sound._paused) {
585
+ if (!internal) {
586
+ self._loadQueue("play");
587
+ }
588
+ return sound._id;
589
+ }
590
+ if (self._webAudio) {
591
+ Howler3._autoResume();
592
+ }
593
+ var seek = Math.max(0, sound._seek > 0 ? sound._seek : self._sprite[sprite][0] / 1e3);
594
+ var duration = Math.max(0, (self._sprite[sprite][0] + self._sprite[sprite][1]) / 1e3 - seek);
595
+ var timeout = duration * 1e3 / Math.abs(sound._rate);
596
+ var start = self._sprite[sprite][0] / 1e3;
597
+ var stop = (self._sprite[sprite][0] + self._sprite[sprite][1]) / 1e3;
598
+ sound._sprite = sprite;
599
+ sound._ended = false;
600
+ var setParams = function() {
601
+ sound._paused = false;
602
+ sound._seek = seek;
603
+ sound._start = start;
604
+ sound._stop = stop;
605
+ sound._loop = !!(sound._loop || self._sprite[sprite][2]);
606
+ };
607
+ if (seek >= stop) {
608
+ self._ended(sound);
609
+ return;
610
+ }
611
+ var node = sound._node;
612
+ if (self._webAudio) {
613
+ var playWebAudio = function() {
614
+ self._playLock = false;
615
+ setParams();
616
+ self._refreshBuffer(sound);
617
+ var vol = sound._muted || self._muted ? 0 : sound._volume;
618
+ node.gain.setValueAtTime(vol, Howler3.ctx.currentTime);
619
+ sound._playStart = Howler3.ctx.currentTime;
620
+ if (typeof node.bufferSource.start === "undefined") {
621
+ sound._loop ? node.bufferSource.noteGrainOn(0, seek, 86400) : node.bufferSource.noteGrainOn(0, seek, duration);
622
+ } else {
623
+ sound._loop ? node.bufferSource.start(0, seek, 86400) : node.bufferSource.start(0, seek, duration);
624
+ }
625
+ if (timeout !== Infinity) {
626
+ self._endTimers[sound._id] = setTimeout(self._ended.bind(self, sound), timeout);
627
+ }
628
+ if (!internal) {
629
+ setTimeout(function() {
630
+ self._emit("play", sound._id);
631
+ self._loadQueue();
632
+ }, 0);
633
+ }
634
+ };
635
+ if (Howler3.state === "running" && Howler3.ctx.state !== "interrupted") {
636
+ playWebAudio();
637
+ } else {
638
+ self._playLock = true;
639
+ self.once("resume", playWebAudio);
640
+ self._clearTimer(sound._id);
641
+ }
642
+ } else {
643
+ var playHtml5 = function() {
644
+ node.currentTime = seek;
645
+ node.muted = sound._muted || self._muted || Howler3._muted || node.muted;
646
+ node.volume = sound._volume * Howler3.volume();
647
+ node.playbackRate = sound._rate;
648
+ try {
649
+ var play = node.play();
650
+ if (play && typeof Promise !== "undefined" && (play instanceof Promise || typeof play.then === "function")) {
651
+ self._playLock = true;
652
+ setParams();
653
+ play.then(function() {
654
+ self._playLock = false;
655
+ node._unlocked = true;
656
+ if (!internal) {
657
+ self._emit("play", sound._id);
658
+ } else {
659
+ self._loadQueue();
660
+ }
661
+ }).catch(function() {
662
+ self._playLock = false;
663
+ self._emit("playerror", sound._id, "Playback was unable to start. This is most commonly an issue on mobile devices and Chrome where playback was not within a user interaction.");
664
+ sound._ended = true;
665
+ sound._paused = true;
666
+ });
667
+ } else if (!internal) {
668
+ self._playLock = false;
669
+ setParams();
670
+ self._emit("play", sound._id);
671
+ }
672
+ node.playbackRate = sound._rate;
673
+ if (node.paused) {
674
+ self._emit("playerror", sound._id, "Playback was unable to start. This is most commonly an issue on mobile devices and Chrome where playback was not within a user interaction.");
675
+ return;
676
+ }
677
+ if (sprite !== "__default" || sound._loop) {
678
+ self._endTimers[sound._id] = setTimeout(self._ended.bind(self, sound), timeout);
679
+ } else {
680
+ self._endTimers[sound._id] = function() {
681
+ self._ended(sound);
682
+ node.removeEventListener("ended", self._endTimers[sound._id], false);
683
+ };
684
+ node.addEventListener("ended", self._endTimers[sound._id], false);
685
+ }
686
+ } catch (err) {
687
+ self._emit("playerror", sound._id, err);
688
+ }
689
+ };
690
+ if (node.src === "data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA") {
691
+ node.src = self._src;
692
+ node.load();
693
+ }
694
+ var loadedNoReadyState = window && window.ejecta || !node.readyState && Howler3._navigator.isCocoonJS;
695
+ if (node.readyState >= 3 || loadedNoReadyState) {
696
+ playHtml5();
697
+ } else {
698
+ self._playLock = true;
699
+ self._state = "loading";
700
+ var listener = function() {
701
+ self._state = "loaded";
702
+ playHtml5();
703
+ node.removeEventListener(Howler3._canPlayEvent, listener, false);
704
+ };
705
+ node.addEventListener(Howler3._canPlayEvent, listener, false);
706
+ self._clearTimer(sound._id);
707
+ }
708
+ }
709
+ return sound._id;
710
+ },
711
+ /**
712
+ * Pause playback and save current position.
713
+ * @param {Number} id The sound ID (empty to pause all in group).
714
+ * @return {Howl}
715
+ */
716
+ pause: function(id) {
717
+ var self = this;
718
+ if (self._state !== "loaded" || self._playLock) {
719
+ self._queue.push({
720
+ event: "pause",
721
+ action: function() {
722
+ self.pause(id);
723
+ }
724
+ });
725
+ return self;
726
+ }
727
+ var ids = self._getSoundIds(id);
728
+ for (var i = 0; i < ids.length; i++) {
729
+ self._clearTimer(ids[i]);
730
+ var sound = self._soundById(ids[i]);
731
+ if (sound && !sound._paused) {
732
+ sound._seek = self.seek(ids[i]);
733
+ sound._rateSeek = 0;
734
+ sound._paused = true;
735
+ self._stopFade(ids[i]);
736
+ if (sound._node) {
737
+ if (self._webAudio) {
738
+ if (!sound._node.bufferSource) {
739
+ continue;
740
+ }
741
+ if (typeof sound._node.bufferSource.stop === "undefined") {
742
+ sound._node.bufferSource.noteOff(0);
743
+ } else {
744
+ sound._node.bufferSource.stop(0);
745
+ }
746
+ self._cleanBuffer(sound._node);
747
+ } else if (!isNaN(sound._node.duration) || sound._node.duration === Infinity) {
748
+ sound._node.pause();
749
+ }
750
+ }
751
+ }
752
+ if (!arguments[1]) {
753
+ self._emit("pause", sound ? sound._id : null);
754
+ }
755
+ }
756
+ return self;
757
+ },
758
+ /**
759
+ * Stop playback and reset to start.
760
+ * @param {Number} id The sound ID (empty to stop all in group).
761
+ * @param {Boolean} internal Internal Use: true prevents event firing.
762
+ * @return {Howl}
763
+ */
764
+ stop: function(id, internal) {
765
+ var self = this;
766
+ if (self._state !== "loaded" || self._playLock) {
767
+ self._queue.push({
768
+ event: "stop",
769
+ action: function() {
770
+ self.stop(id);
771
+ }
772
+ });
773
+ return self;
774
+ }
775
+ var ids = self._getSoundIds(id);
776
+ for (var i = 0; i < ids.length; i++) {
777
+ self._clearTimer(ids[i]);
778
+ var sound = self._soundById(ids[i]);
779
+ if (sound) {
780
+ sound._seek = sound._start || 0;
781
+ sound._rateSeek = 0;
782
+ sound._paused = true;
783
+ sound._ended = true;
784
+ self._stopFade(ids[i]);
785
+ if (sound._node) {
786
+ if (self._webAudio) {
787
+ if (sound._node.bufferSource) {
788
+ if (typeof sound._node.bufferSource.stop === "undefined") {
789
+ sound._node.bufferSource.noteOff(0);
790
+ } else {
791
+ sound._node.bufferSource.stop(0);
792
+ }
793
+ self._cleanBuffer(sound._node);
794
+ }
795
+ } else if (!isNaN(sound._node.duration) || sound._node.duration === Infinity) {
796
+ sound._node.currentTime = sound._start || 0;
797
+ sound._node.pause();
798
+ if (sound._node.duration === Infinity) {
799
+ self._clearSound(sound._node);
800
+ }
801
+ }
802
+ }
803
+ if (!internal) {
804
+ self._emit("stop", sound._id);
805
+ }
806
+ }
807
+ }
808
+ return self;
809
+ },
810
+ /**
811
+ * Mute/unmute a single sound or all sounds in this Howl group.
812
+ * @param {Boolean} muted Set to true to mute and false to unmute.
813
+ * @param {Number} id The sound ID to update (omit to mute/unmute all).
814
+ * @return {Howl}
815
+ */
816
+ mute: function(muted, id) {
817
+ var self = this;
818
+ if (self._state !== "loaded" || self._playLock) {
819
+ self._queue.push({
820
+ event: "mute",
821
+ action: function() {
822
+ self.mute(muted, id);
823
+ }
824
+ });
825
+ return self;
826
+ }
827
+ if (typeof id === "undefined") {
828
+ if (typeof muted === "boolean") {
829
+ self._muted = muted;
830
+ } else {
831
+ return self._muted;
832
+ }
833
+ }
834
+ var ids = self._getSoundIds(id);
835
+ for (var i = 0; i < ids.length; i++) {
836
+ var sound = self._soundById(ids[i]);
837
+ if (sound) {
838
+ sound._muted = muted;
839
+ if (sound._interval) {
840
+ self._stopFade(sound._id);
841
+ }
842
+ if (self._webAudio && sound._node) {
843
+ sound._node.gain.setValueAtTime(muted ? 0 : sound._volume, Howler3.ctx.currentTime);
844
+ } else if (sound._node) {
845
+ sound._node.muted = Howler3._muted ? true : muted;
846
+ }
847
+ self._emit("mute", sound._id);
848
+ }
849
+ }
850
+ return self;
851
+ },
852
+ /**
853
+ * Get/set the volume of this sound or of the Howl group. This method can optionally take 0, 1 or 2 arguments.
854
+ * volume() -> Returns the group's volume value.
855
+ * volume(id) -> Returns the sound id's current volume.
856
+ * volume(vol) -> Sets the volume of all sounds in this Howl group.
857
+ * volume(vol, id) -> Sets the volume of passed sound id.
858
+ * @return {Howl/Number} Returns self or current volume.
859
+ */
860
+ volume: function() {
861
+ var self = this;
862
+ var args = arguments;
863
+ var vol, id;
864
+ if (args.length === 0) {
865
+ return self._volume;
866
+ } else if (args.length === 1 || args.length === 2 && typeof args[1] === "undefined") {
867
+ var ids = self._getSoundIds();
868
+ var index = ids.indexOf(args[0]);
869
+ if (index >= 0) {
870
+ id = parseInt(args[0], 10);
871
+ } else {
872
+ vol = parseFloat(args[0]);
873
+ }
874
+ } else if (args.length >= 2) {
875
+ vol = parseFloat(args[0]);
876
+ id = parseInt(args[1], 10);
877
+ }
878
+ var sound;
879
+ if (typeof vol !== "undefined" && vol >= 0 && vol <= 1) {
880
+ if (self._state !== "loaded" || self._playLock) {
881
+ self._queue.push({
882
+ event: "volume",
883
+ action: function() {
884
+ self.volume.apply(self, args);
885
+ }
886
+ });
887
+ return self;
888
+ }
889
+ if (typeof id === "undefined") {
890
+ self._volume = vol;
891
+ }
892
+ id = self._getSoundIds(id);
893
+ for (var i = 0; i < id.length; i++) {
894
+ sound = self._soundById(id[i]);
895
+ if (sound) {
896
+ sound._volume = vol;
897
+ if (!args[2]) {
898
+ self._stopFade(id[i]);
899
+ }
900
+ if (self._webAudio && sound._node && !sound._muted) {
901
+ sound._node.gain.setValueAtTime(vol, Howler3.ctx.currentTime);
902
+ } else if (sound._node && !sound._muted) {
903
+ sound._node.volume = vol * Howler3.volume();
904
+ }
905
+ self._emit("volume", sound._id);
906
+ }
907
+ }
908
+ } else {
909
+ sound = id ? self._soundById(id) : self._sounds[0];
910
+ return sound ? sound._volume : 0;
911
+ }
912
+ return self;
913
+ },
914
+ /**
915
+ * Fade a currently playing sound between two volumes (if no id is passed, all sounds will fade).
916
+ * @param {Number} from The value to fade from (0.0 to 1.0).
917
+ * @param {Number} to The volume to fade to (0.0 to 1.0).
918
+ * @param {Number} len Time in milliseconds to fade.
919
+ * @param {Number} id The sound id (omit to fade all sounds).
920
+ * @return {Howl}
921
+ */
922
+ fade: function(from, to, len, id) {
923
+ var self = this;
924
+ if (self._state !== "loaded" || self._playLock) {
925
+ self._queue.push({
926
+ event: "fade",
927
+ action: function() {
928
+ self.fade(from, to, len, id);
929
+ }
930
+ });
931
+ return self;
932
+ }
933
+ from = Math.min(Math.max(0, parseFloat(from)), 1);
934
+ to = Math.min(Math.max(0, parseFloat(to)), 1);
935
+ len = parseFloat(len);
936
+ self.volume(from, id);
937
+ var ids = self._getSoundIds(id);
938
+ for (var i = 0; i < ids.length; i++) {
939
+ var sound = self._soundById(ids[i]);
940
+ if (sound) {
941
+ if (!id) {
942
+ self._stopFade(ids[i]);
943
+ }
944
+ if (self._webAudio && !sound._muted) {
945
+ var currentTime = Howler3.ctx.currentTime;
946
+ var end = currentTime + len / 1e3;
947
+ sound._volume = from;
948
+ sound._node.gain.setValueAtTime(from, currentTime);
949
+ sound._node.gain.linearRampToValueAtTime(to, end);
950
+ }
951
+ self._startFadeInterval(sound, from, to, len, ids[i], typeof id === "undefined");
952
+ }
953
+ }
954
+ return self;
955
+ },
956
+ /**
957
+ * Starts the internal interval to fade a sound.
958
+ * @param {Object} sound Reference to sound to fade.
959
+ * @param {Number} from The value to fade from (0.0 to 1.0).
960
+ * @param {Number} to The volume to fade to (0.0 to 1.0).
961
+ * @param {Number} len Time in milliseconds to fade.
962
+ * @param {Number} id The sound id to fade.
963
+ * @param {Boolean} isGroup If true, set the volume on the group.
964
+ */
965
+ _startFadeInterval: function(sound, from, to, len, id, isGroup) {
966
+ var self = this;
967
+ var vol = from;
968
+ var diff = to - from;
969
+ var steps = Math.abs(diff / 0.01);
970
+ var stepLen = Math.max(4, steps > 0 ? len / steps : len);
971
+ var lastTick = Date.now();
972
+ sound._fadeTo = to;
973
+ sound._interval = setInterval(function() {
974
+ var tick = (Date.now() - lastTick) / len;
975
+ lastTick = Date.now();
976
+ vol += diff * tick;
977
+ vol = Math.round(vol * 100) / 100;
978
+ if (diff < 0) {
979
+ vol = Math.max(to, vol);
980
+ } else {
981
+ vol = Math.min(to, vol);
982
+ }
983
+ if (self._webAudio) {
984
+ sound._volume = vol;
985
+ } else {
986
+ self.volume(vol, sound._id, true);
987
+ }
988
+ if (isGroup) {
989
+ self._volume = vol;
990
+ }
991
+ if (to < from && vol <= to || to > from && vol >= to) {
992
+ clearInterval(sound._interval);
993
+ sound._interval = null;
994
+ sound._fadeTo = null;
995
+ self.volume(to, sound._id);
996
+ self._emit("fade", sound._id);
997
+ }
998
+ }, stepLen);
999
+ },
1000
+ /**
1001
+ * Internal method that stops the currently playing fade when
1002
+ * a new fade starts, volume is changed or the sound is stopped.
1003
+ * @param {Number} id The sound id.
1004
+ * @return {Howl}
1005
+ */
1006
+ _stopFade: function(id) {
1007
+ var self = this;
1008
+ var sound = self._soundById(id);
1009
+ if (sound && sound._interval) {
1010
+ if (self._webAudio) {
1011
+ sound._node.gain.cancelScheduledValues(Howler3.ctx.currentTime);
1012
+ }
1013
+ clearInterval(sound._interval);
1014
+ sound._interval = null;
1015
+ self.volume(sound._fadeTo, id);
1016
+ sound._fadeTo = null;
1017
+ self._emit("fade", id);
1018
+ }
1019
+ return self;
1020
+ },
1021
+ /**
1022
+ * Get/set the loop parameter on a sound. This method can optionally take 0, 1 or 2 arguments.
1023
+ * loop() -> Returns the group's loop value.
1024
+ * loop(id) -> Returns the sound id's loop value.
1025
+ * loop(loop) -> Sets the loop value for all sounds in this Howl group.
1026
+ * loop(loop, id) -> Sets the loop value of passed sound id.
1027
+ * @return {Howl/Boolean} Returns self or current loop value.
1028
+ */
1029
+ loop: function() {
1030
+ var self = this;
1031
+ var args = arguments;
1032
+ var loop, id, sound;
1033
+ if (args.length === 0) {
1034
+ return self._loop;
1035
+ } else if (args.length === 1) {
1036
+ if (typeof args[0] === "boolean") {
1037
+ loop = args[0];
1038
+ self._loop = loop;
1039
+ } else {
1040
+ sound = self._soundById(parseInt(args[0], 10));
1041
+ return sound ? sound._loop : false;
1042
+ }
1043
+ } else if (args.length === 2) {
1044
+ loop = args[0];
1045
+ id = parseInt(args[1], 10);
1046
+ }
1047
+ var ids = self._getSoundIds(id);
1048
+ for (var i = 0; i < ids.length; i++) {
1049
+ sound = self._soundById(ids[i]);
1050
+ if (sound) {
1051
+ sound._loop = loop;
1052
+ if (self._webAudio && sound._node && sound._node.bufferSource) {
1053
+ sound._node.bufferSource.loop = loop;
1054
+ if (loop) {
1055
+ sound._node.bufferSource.loopStart = sound._start || 0;
1056
+ sound._node.bufferSource.loopEnd = sound._stop;
1057
+ if (self.playing(ids[i])) {
1058
+ self.pause(ids[i], true);
1059
+ self.play(ids[i], true);
1060
+ }
1061
+ }
1062
+ }
1063
+ }
1064
+ }
1065
+ return self;
1066
+ },
1067
+ /**
1068
+ * Get/set the playback rate of a sound. This method can optionally take 0, 1 or 2 arguments.
1069
+ * rate() -> Returns the first sound node's current playback rate.
1070
+ * rate(id) -> Returns the sound id's current playback rate.
1071
+ * rate(rate) -> Sets the playback rate of all sounds in this Howl group.
1072
+ * rate(rate, id) -> Sets the playback rate of passed sound id.
1073
+ * @return {Howl/Number} Returns self or the current playback rate.
1074
+ */
1075
+ rate: function() {
1076
+ var self = this;
1077
+ var args = arguments;
1078
+ var rate, id;
1079
+ if (args.length === 0) {
1080
+ id = self._sounds[0]._id;
1081
+ } else if (args.length === 1) {
1082
+ var ids = self._getSoundIds();
1083
+ var index = ids.indexOf(args[0]);
1084
+ if (index >= 0) {
1085
+ id = parseInt(args[0], 10);
1086
+ } else {
1087
+ rate = parseFloat(args[0]);
1088
+ }
1089
+ } else if (args.length === 2) {
1090
+ rate = parseFloat(args[0]);
1091
+ id = parseInt(args[1], 10);
1092
+ }
1093
+ var sound;
1094
+ if (typeof rate === "number") {
1095
+ if (self._state !== "loaded" || self._playLock) {
1096
+ self._queue.push({
1097
+ event: "rate",
1098
+ action: function() {
1099
+ self.rate.apply(self, args);
1100
+ }
1101
+ });
1102
+ return self;
1103
+ }
1104
+ if (typeof id === "undefined") {
1105
+ self._rate = rate;
1106
+ }
1107
+ id = self._getSoundIds(id);
1108
+ for (var i = 0; i < id.length; i++) {
1109
+ sound = self._soundById(id[i]);
1110
+ if (sound) {
1111
+ if (self.playing(id[i])) {
1112
+ sound._rateSeek = self.seek(id[i]);
1113
+ sound._playStart = self._webAudio ? Howler3.ctx.currentTime : sound._playStart;
1114
+ }
1115
+ sound._rate = rate;
1116
+ if (self._webAudio && sound._node && sound._node.bufferSource) {
1117
+ sound._node.bufferSource.playbackRate.setValueAtTime(rate, Howler3.ctx.currentTime);
1118
+ } else if (sound._node) {
1119
+ sound._node.playbackRate = rate;
1120
+ }
1121
+ var seek = self.seek(id[i]);
1122
+ var duration = (self._sprite[sound._sprite][0] + self._sprite[sound._sprite][1]) / 1e3 - seek;
1123
+ var timeout = duration * 1e3 / Math.abs(sound._rate);
1124
+ if (self._endTimers[id[i]] || !sound._paused) {
1125
+ self._clearTimer(id[i]);
1126
+ self._endTimers[id[i]] = setTimeout(self._ended.bind(self, sound), timeout);
1127
+ }
1128
+ self._emit("rate", sound._id);
1129
+ }
1130
+ }
1131
+ } else {
1132
+ sound = self._soundById(id);
1133
+ return sound ? sound._rate : self._rate;
1134
+ }
1135
+ return self;
1136
+ },
1137
+ /**
1138
+ * Get/set the seek position of a sound. This method can optionally take 0, 1 or 2 arguments.
1139
+ * seek() -> Returns the first sound node's current seek position.
1140
+ * seek(id) -> Returns the sound id's current seek position.
1141
+ * seek(seek) -> Sets the seek position of the first sound node.
1142
+ * seek(seek, id) -> Sets the seek position of passed sound id.
1143
+ * @return {Howl/Number} Returns self or the current seek position.
1144
+ */
1145
+ seek: function() {
1146
+ var self = this;
1147
+ var args = arguments;
1148
+ var seek, id;
1149
+ if (args.length === 0) {
1150
+ if (self._sounds.length) {
1151
+ id = self._sounds[0]._id;
1152
+ }
1153
+ } else if (args.length === 1) {
1154
+ var ids = self._getSoundIds();
1155
+ var index = ids.indexOf(args[0]);
1156
+ if (index >= 0) {
1157
+ id = parseInt(args[0], 10);
1158
+ } else if (self._sounds.length) {
1159
+ id = self._sounds[0]._id;
1160
+ seek = parseFloat(args[0]);
1161
+ }
1162
+ } else if (args.length === 2) {
1163
+ seek = parseFloat(args[0]);
1164
+ id = parseInt(args[1], 10);
1165
+ }
1166
+ if (typeof id === "undefined") {
1167
+ return 0;
1168
+ }
1169
+ if (typeof seek === "number" && (self._state !== "loaded" || self._playLock)) {
1170
+ self._queue.push({
1171
+ event: "seek",
1172
+ action: function() {
1173
+ self.seek.apply(self, args);
1174
+ }
1175
+ });
1176
+ return self;
1177
+ }
1178
+ var sound = self._soundById(id);
1179
+ if (sound) {
1180
+ if (typeof seek === "number" && seek >= 0) {
1181
+ var playing = self.playing(id);
1182
+ if (playing) {
1183
+ self.pause(id, true);
1184
+ }
1185
+ sound._seek = seek;
1186
+ sound._ended = false;
1187
+ self._clearTimer(id);
1188
+ if (!self._webAudio && sound._node && !isNaN(sound._node.duration)) {
1189
+ sound._node.currentTime = seek;
1190
+ }
1191
+ var seekAndEmit = function() {
1192
+ if (playing) {
1193
+ self.play(id, true);
1194
+ }
1195
+ self._emit("seek", id);
1196
+ };
1197
+ if (playing && !self._webAudio) {
1198
+ var emitSeek = function() {
1199
+ if (!self._playLock) {
1200
+ seekAndEmit();
1201
+ } else {
1202
+ setTimeout(emitSeek, 0);
1203
+ }
1204
+ };
1205
+ setTimeout(emitSeek, 0);
1206
+ } else {
1207
+ seekAndEmit();
1208
+ }
1209
+ } else {
1210
+ if (self._webAudio) {
1211
+ var realTime = self.playing(id) ? Howler3.ctx.currentTime - sound._playStart : 0;
1212
+ var rateSeek = sound._rateSeek ? sound._rateSeek - sound._seek : 0;
1213
+ return sound._seek + (rateSeek + realTime * Math.abs(sound._rate));
1214
+ } else {
1215
+ return sound._node.currentTime;
1216
+ }
1217
+ }
1218
+ }
1219
+ return self;
1220
+ },
1221
+ /**
1222
+ * Check if a specific sound is currently playing or not (if id is provided), or check if at least one of the sounds in the group is playing or not.
1223
+ * @param {Number} id The sound id to check. If none is passed, the whole sound group is checked.
1224
+ * @return {Boolean} True if playing and false if not.
1225
+ */
1226
+ playing: function(id) {
1227
+ var self = this;
1228
+ if (typeof id === "number") {
1229
+ var sound = self._soundById(id);
1230
+ return sound ? !sound._paused : false;
1231
+ }
1232
+ for (var i = 0; i < self._sounds.length; i++) {
1233
+ if (!self._sounds[i]._paused) {
1234
+ return true;
1235
+ }
1236
+ }
1237
+ return false;
1238
+ },
1239
+ /**
1240
+ * Get the duration of this sound. Passing a sound id will return the sprite duration.
1241
+ * @param {Number} id The sound id to check. If none is passed, return full source duration.
1242
+ * @return {Number} Audio duration in seconds.
1243
+ */
1244
+ duration: function(id) {
1245
+ var self = this;
1246
+ var duration = self._duration;
1247
+ var sound = self._soundById(id);
1248
+ if (sound) {
1249
+ duration = self._sprite[sound._sprite][1] / 1e3;
1250
+ }
1251
+ return duration;
1252
+ },
1253
+ /**
1254
+ * Returns the current loaded state of this Howl.
1255
+ * @return {String} 'unloaded', 'loading', 'loaded'
1256
+ */
1257
+ state: function() {
1258
+ return this._state;
1259
+ },
1260
+ /**
1261
+ * Unload and destroy the current Howl object.
1262
+ * This will immediately stop all sound instances attached to this group.
1263
+ */
1264
+ unload: function() {
1265
+ var self = this;
1266
+ var sounds = self._sounds;
1267
+ for (var i = 0; i < sounds.length; i++) {
1268
+ if (!sounds[i]._paused) {
1269
+ self.stop(sounds[i]._id);
1270
+ }
1271
+ if (!self._webAudio) {
1272
+ self._clearSound(sounds[i]._node);
1273
+ sounds[i]._node.removeEventListener("error", sounds[i]._errorFn, false);
1274
+ sounds[i]._node.removeEventListener(Howler3._canPlayEvent, sounds[i]._loadFn, false);
1275
+ sounds[i]._node.removeEventListener("ended", sounds[i]._endFn, false);
1276
+ Howler3._releaseHtml5Audio(sounds[i]._node);
1277
+ }
1278
+ delete sounds[i]._node;
1279
+ self._clearTimer(sounds[i]._id);
1280
+ }
1281
+ var index = Howler3._howls.indexOf(self);
1282
+ if (index >= 0) {
1283
+ Howler3._howls.splice(index, 1);
1284
+ }
1285
+ var remCache = true;
1286
+ for (i = 0; i < Howler3._howls.length; i++) {
1287
+ if (Howler3._howls[i]._src === self._src || self._src.indexOf(Howler3._howls[i]._src) >= 0) {
1288
+ remCache = false;
1289
+ break;
1290
+ }
1291
+ }
1292
+ if (cache && remCache) {
1293
+ delete cache[self._src];
1294
+ }
1295
+ Howler3.noAudio = false;
1296
+ self._state = "unloaded";
1297
+ self._sounds = [];
1298
+ self = null;
1299
+ return null;
1300
+ },
1301
+ /**
1302
+ * Listen to a custom event.
1303
+ * @param {String} event Event name.
1304
+ * @param {Function} fn Listener to call.
1305
+ * @param {Number} id (optional) Only listen to events for this sound.
1306
+ * @param {Number} once (INTERNAL) Marks event to fire only once.
1307
+ * @return {Howl}
1308
+ */
1309
+ on: function(event, fn, id, once) {
1310
+ var self = this;
1311
+ var events = self["_on" + event];
1312
+ if (typeof fn === "function") {
1313
+ events.push(once ? { id, fn, once } : { id, fn });
1314
+ }
1315
+ return self;
1316
+ },
1317
+ /**
1318
+ * Remove a custom event. Call without parameters to remove all events.
1319
+ * @param {String} event Event name.
1320
+ * @param {Function} fn Listener to remove. Leave empty to remove all.
1321
+ * @param {Number} id (optional) Only remove events for this sound.
1322
+ * @return {Howl}
1323
+ */
1324
+ off: function(event, fn, id) {
1325
+ var self = this;
1326
+ var events = self["_on" + event];
1327
+ var i = 0;
1328
+ if (typeof fn === "number") {
1329
+ id = fn;
1330
+ fn = null;
1331
+ }
1332
+ if (fn || id) {
1333
+ for (i = 0; i < events.length; i++) {
1334
+ var isId = id === events[i].id;
1335
+ if (fn === events[i].fn && isId || !fn && isId) {
1336
+ events.splice(i, 1);
1337
+ break;
1338
+ }
1339
+ }
1340
+ } else if (event) {
1341
+ self["_on" + event] = [];
1342
+ } else {
1343
+ var keys = Object.keys(self);
1344
+ for (i = 0; i < keys.length; i++) {
1345
+ if (keys[i].indexOf("_on") === 0 && Array.isArray(self[keys[i]])) {
1346
+ self[keys[i]] = [];
1347
+ }
1348
+ }
1349
+ }
1350
+ return self;
1351
+ },
1352
+ /**
1353
+ * Listen to a custom event and remove it once fired.
1354
+ * @param {String} event Event name.
1355
+ * @param {Function} fn Listener to call.
1356
+ * @param {Number} id (optional) Only listen to events for this sound.
1357
+ * @return {Howl}
1358
+ */
1359
+ once: function(event, fn, id) {
1360
+ var self = this;
1361
+ self.on(event, fn, id, 1);
1362
+ return self;
1363
+ },
1364
+ /**
1365
+ * Emit all events of a specific type and pass the sound id.
1366
+ * @param {String} event Event name.
1367
+ * @param {Number} id Sound ID.
1368
+ * @param {Number} msg Message to go with event.
1369
+ * @return {Howl}
1370
+ */
1371
+ _emit: function(event, id, msg) {
1372
+ var self = this;
1373
+ var events = self["_on" + event];
1374
+ for (var i = events.length - 1; i >= 0; i--) {
1375
+ if (!events[i].id || events[i].id === id || event === "load") {
1376
+ setTimeout(function(fn) {
1377
+ fn.call(this, id, msg);
1378
+ }.bind(self, events[i].fn), 0);
1379
+ if (events[i].once) {
1380
+ self.off(event, events[i].fn, events[i].id);
1381
+ }
1382
+ }
1383
+ }
1384
+ self._loadQueue(event);
1385
+ return self;
1386
+ },
1387
+ /**
1388
+ * Queue of actions initiated before the sound has loaded.
1389
+ * These will be called in sequence, with the next only firing
1390
+ * after the previous has finished executing (even if async like play).
1391
+ * @return {Howl}
1392
+ */
1393
+ _loadQueue: function(event) {
1394
+ var self = this;
1395
+ if (self._queue.length > 0) {
1396
+ var task = self._queue[0];
1397
+ if (task.event === event) {
1398
+ self._queue.shift();
1399
+ self._loadQueue();
1400
+ }
1401
+ if (!event) {
1402
+ task.action();
1403
+ }
1404
+ }
1405
+ return self;
1406
+ },
1407
+ /**
1408
+ * Fired when playback ends at the end of the duration.
1409
+ * @param {Sound} sound The sound object to work with.
1410
+ * @return {Howl}
1411
+ */
1412
+ _ended: function(sound) {
1413
+ var self = this;
1414
+ var sprite = sound._sprite;
1415
+ if (!self._webAudio && sound._node && !sound._node.paused && !sound._node.ended && sound._node.currentTime < sound._stop) {
1416
+ setTimeout(self._ended.bind(self, sound), 100);
1417
+ return self;
1418
+ }
1419
+ var loop = !!(sound._loop || self._sprite[sprite][2]);
1420
+ self._emit("end", sound._id);
1421
+ if (!self._webAudio && loop) {
1422
+ self.stop(sound._id, true).play(sound._id);
1423
+ }
1424
+ if (self._webAudio && loop) {
1425
+ self._emit("play", sound._id);
1426
+ sound._seek = sound._start || 0;
1427
+ sound._rateSeek = 0;
1428
+ sound._playStart = Howler3.ctx.currentTime;
1429
+ var timeout = (sound._stop - sound._start) * 1e3 / Math.abs(sound._rate);
1430
+ self._endTimers[sound._id] = setTimeout(self._ended.bind(self, sound), timeout);
1431
+ }
1432
+ if (self._webAudio && !loop) {
1433
+ sound._paused = true;
1434
+ sound._ended = true;
1435
+ sound._seek = sound._start || 0;
1436
+ sound._rateSeek = 0;
1437
+ self._clearTimer(sound._id);
1438
+ self._cleanBuffer(sound._node);
1439
+ Howler3._autoSuspend();
1440
+ }
1441
+ if (!self._webAudio && !loop) {
1442
+ self.stop(sound._id, true);
1443
+ }
1444
+ return self;
1445
+ },
1446
+ /**
1447
+ * Clear the end timer for a sound playback.
1448
+ * @param {Number} id The sound ID.
1449
+ * @return {Howl}
1450
+ */
1451
+ _clearTimer: function(id) {
1452
+ var self = this;
1453
+ if (self._endTimers[id]) {
1454
+ if (typeof self._endTimers[id] !== "function") {
1455
+ clearTimeout(self._endTimers[id]);
1456
+ } else {
1457
+ var sound = self._soundById(id);
1458
+ if (sound && sound._node) {
1459
+ sound._node.removeEventListener("ended", self._endTimers[id], false);
1460
+ }
1461
+ }
1462
+ delete self._endTimers[id];
1463
+ }
1464
+ return self;
1465
+ },
1466
+ /**
1467
+ * Return the sound identified by this ID, or return null.
1468
+ * @param {Number} id Sound ID
1469
+ * @return {Object} Sound object or null.
1470
+ */
1471
+ _soundById: function(id) {
1472
+ var self = this;
1473
+ for (var i = 0; i < self._sounds.length; i++) {
1474
+ if (id === self._sounds[i]._id) {
1475
+ return self._sounds[i];
1476
+ }
1477
+ }
1478
+ return null;
1479
+ },
1480
+ /**
1481
+ * Return an inactive sound from the pool or create a new one.
1482
+ * @return {Sound} Sound playback object.
1483
+ */
1484
+ _inactiveSound: function() {
1485
+ var self = this;
1486
+ self._drain();
1487
+ for (var i = 0; i < self._sounds.length; i++) {
1488
+ if (self._sounds[i]._ended) {
1489
+ return self._sounds[i].reset();
1490
+ }
1491
+ }
1492
+ return new Sound2(self);
1493
+ },
1494
+ /**
1495
+ * Drain excess inactive sounds from the pool.
1496
+ */
1497
+ _drain: function() {
1498
+ var self = this;
1499
+ var limit = self._pool;
1500
+ var cnt = 0;
1501
+ var i = 0;
1502
+ if (self._sounds.length < limit) {
1503
+ return;
1504
+ }
1505
+ for (i = 0; i < self._sounds.length; i++) {
1506
+ if (self._sounds[i]._ended) {
1507
+ cnt++;
1508
+ }
1509
+ }
1510
+ for (i = self._sounds.length - 1; i >= 0; i--) {
1511
+ if (cnt <= limit) {
1512
+ return;
1513
+ }
1514
+ if (self._sounds[i]._ended) {
1515
+ if (self._webAudio && self._sounds[i]._node) {
1516
+ self._sounds[i]._node.disconnect(0);
1517
+ }
1518
+ self._sounds.splice(i, 1);
1519
+ cnt--;
1520
+ }
1521
+ }
1522
+ },
1523
+ /**
1524
+ * Get all ID's from the sounds pool.
1525
+ * @param {Number} id Only return one ID if one is passed.
1526
+ * @return {Array} Array of IDs.
1527
+ */
1528
+ _getSoundIds: function(id) {
1529
+ var self = this;
1530
+ if (typeof id === "undefined") {
1531
+ var ids = [];
1532
+ for (var i = 0; i < self._sounds.length; i++) {
1533
+ ids.push(self._sounds[i]._id);
1534
+ }
1535
+ return ids;
1536
+ } else {
1537
+ return [id];
1538
+ }
1539
+ },
1540
+ /**
1541
+ * Load the sound back into the buffer source.
1542
+ * @param {Sound} sound The sound object to work with.
1543
+ * @return {Howl}
1544
+ */
1545
+ _refreshBuffer: function(sound) {
1546
+ var self = this;
1547
+ sound._node.bufferSource = Howler3.ctx.createBufferSource();
1548
+ sound._node.bufferSource.buffer = cache[self._src];
1549
+ if (sound._panner) {
1550
+ sound._node.bufferSource.connect(sound._panner);
1551
+ } else {
1552
+ sound._node.bufferSource.connect(sound._node);
1553
+ }
1554
+ sound._node.bufferSource.loop = sound._loop;
1555
+ if (sound._loop) {
1556
+ sound._node.bufferSource.loopStart = sound._start || 0;
1557
+ sound._node.bufferSource.loopEnd = sound._stop || 0;
1558
+ }
1559
+ sound._node.bufferSource.playbackRate.setValueAtTime(sound._rate, Howler3.ctx.currentTime);
1560
+ return self;
1561
+ },
1562
+ /**
1563
+ * Prevent memory leaks by cleaning up the buffer source after playback.
1564
+ * @param {Object} node Sound's audio node containing the buffer source.
1565
+ * @return {Howl}
1566
+ */
1567
+ _cleanBuffer: function(node) {
1568
+ var self = this;
1569
+ var isIOS = Howler3._navigator && Howler3._navigator.vendor.indexOf("Apple") >= 0;
1570
+ if (!node.bufferSource) {
1571
+ return self;
1572
+ }
1573
+ if (Howler3._scratchBuffer && node.bufferSource) {
1574
+ node.bufferSource.onended = null;
1575
+ node.bufferSource.disconnect(0);
1576
+ if (isIOS) {
1577
+ try {
1578
+ node.bufferSource.buffer = Howler3._scratchBuffer;
1579
+ } catch (e) {
1580
+ }
1581
+ }
1582
+ }
1583
+ node.bufferSource = null;
1584
+ return self;
1585
+ },
1586
+ /**
1587
+ * Set the source to a 0-second silence to stop any downloading (except in IE).
1588
+ * @param {Object} node Audio node to clear.
1589
+ */
1590
+ _clearSound: function(node) {
1591
+ var checkIE = /MSIE |Trident\//.test(Howler3._navigator && Howler3._navigator.userAgent);
1592
+ if (!checkIE) {
1593
+ node.src = "data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA";
1594
+ }
1595
+ }
1596
+ };
1597
+ var Sound2 = function(howl) {
1598
+ this._parent = howl;
1599
+ this.init();
1600
+ };
1601
+ Sound2.prototype = {
1602
+ /**
1603
+ * Initialize a new Sound object.
1604
+ * @return {Sound}
1605
+ */
1606
+ init: function() {
1607
+ var self = this;
1608
+ var parent = self._parent;
1609
+ self._muted = parent._muted;
1610
+ self._loop = parent._loop;
1611
+ self._volume = parent._volume;
1612
+ self._rate = parent._rate;
1613
+ self._seek = 0;
1614
+ self._paused = true;
1615
+ self._ended = true;
1616
+ self._sprite = "__default";
1617
+ self._id = ++Howler3._counter;
1618
+ parent._sounds.push(self);
1619
+ self.create();
1620
+ return self;
1621
+ },
1622
+ /**
1623
+ * Create and setup a new sound object, whether HTML5 Audio or Web Audio.
1624
+ * @return {Sound}
1625
+ */
1626
+ create: function() {
1627
+ var self = this;
1628
+ var parent = self._parent;
1629
+ var volume = Howler3._muted || self._muted || self._parent._muted ? 0 : self._volume;
1630
+ if (parent._webAudio) {
1631
+ self._node = typeof Howler3.ctx.createGain === "undefined" ? Howler3.ctx.createGainNode() : Howler3.ctx.createGain();
1632
+ self._node.gain.setValueAtTime(volume, Howler3.ctx.currentTime);
1633
+ self._node.paused = true;
1634
+ self._node.connect(Howler3.masterGain);
1635
+ } else if (!Howler3.noAudio) {
1636
+ self._node = Howler3._obtainHtml5Audio();
1637
+ self._errorFn = self._errorListener.bind(self);
1638
+ self._node.addEventListener("error", self._errorFn, false);
1639
+ self._loadFn = self._loadListener.bind(self);
1640
+ self._node.addEventListener(Howler3._canPlayEvent, self._loadFn, false);
1641
+ self._endFn = self._endListener.bind(self);
1642
+ self._node.addEventListener("ended", self._endFn, false);
1643
+ self._node.src = parent._src;
1644
+ self._node.preload = parent._preload === true ? "auto" : parent._preload;
1645
+ self._node.volume = volume * Howler3.volume();
1646
+ self._node.load();
1647
+ }
1648
+ return self;
1649
+ },
1650
+ /**
1651
+ * Reset the parameters of this sound to the original state (for recycle).
1652
+ * @return {Sound}
1653
+ */
1654
+ reset: function() {
1655
+ var self = this;
1656
+ var parent = self._parent;
1657
+ self._muted = parent._muted;
1658
+ self._loop = parent._loop;
1659
+ self._volume = parent._volume;
1660
+ self._rate = parent._rate;
1661
+ self._seek = 0;
1662
+ self._rateSeek = 0;
1663
+ self._paused = true;
1664
+ self._ended = true;
1665
+ self._sprite = "__default";
1666
+ self._id = ++Howler3._counter;
1667
+ return self;
1668
+ },
1669
+ /**
1670
+ * HTML5 Audio error listener callback.
1671
+ */
1672
+ _errorListener: function() {
1673
+ var self = this;
1674
+ self._parent._emit("loaderror", self._id, self._node.error ? self._node.error.code : 0);
1675
+ self._node.removeEventListener("error", self._errorFn, false);
1676
+ },
1677
+ /**
1678
+ * HTML5 Audio canplaythrough listener callback.
1679
+ */
1680
+ _loadListener: function() {
1681
+ var self = this;
1682
+ var parent = self._parent;
1683
+ parent._duration = Math.ceil(self._node.duration * 10) / 10;
1684
+ if (Object.keys(parent._sprite).length === 0) {
1685
+ parent._sprite = { __default: [0, parent._duration * 1e3] };
1686
+ }
1687
+ if (parent._state !== "loaded") {
1688
+ parent._state = "loaded";
1689
+ parent._emit("load");
1690
+ parent._loadQueue();
1691
+ }
1692
+ self._node.removeEventListener(Howler3._canPlayEvent, self._loadFn, false);
1693
+ },
1694
+ /**
1695
+ * HTML5 Audio ended listener callback.
1696
+ */
1697
+ _endListener: function() {
1698
+ var self = this;
1699
+ var parent = self._parent;
1700
+ if (parent._duration === Infinity) {
1701
+ parent._duration = Math.ceil(self._node.duration * 10) / 10;
1702
+ if (parent._sprite.__default[1] === Infinity) {
1703
+ parent._sprite.__default[1] = parent._duration * 1e3;
1704
+ }
1705
+ parent._ended(self);
1706
+ }
1707
+ self._node.removeEventListener("ended", self._endFn, false);
1708
+ }
1709
+ };
1710
+ var cache = {};
1711
+ var loadBuffer = function(self) {
1712
+ var url = self._src;
1713
+ if (cache[url]) {
1714
+ self._duration = cache[url].duration;
1715
+ loadSound(self);
1716
+ return;
1717
+ }
1718
+ if (/^data:[^;]+;base64,/.test(url)) {
1719
+ var data = atob(url.split(",")[1]);
1720
+ var dataView = new Uint8Array(data.length);
1721
+ for (var i = 0; i < data.length; ++i) {
1722
+ dataView[i] = data.charCodeAt(i);
1723
+ }
1724
+ decodeAudioData(dataView.buffer, self);
1725
+ } else {
1726
+ var xhr = new XMLHttpRequest();
1727
+ xhr.open(self._xhr.method, url, true);
1728
+ xhr.withCredentials = self._xhr.withCredentials;
1729
+ xhr.responseType = "arraybuffer";
1730
+ if (self._xhr.headers) {
1731
+ Object.keys(self._xhr.headers).forEach(function(key) {
1732
+ xhr.setRequestHeader(key, self._xhr.headers[key]);
1733
+ });
1734
+ }
1735
+ xhr.onload = function() {
1736
+ var code = (xhr.status + "")[0];
1737
+ if (code !== "0" && code !== "2" && code !== "3") {
1738
+ self._emit("loaderror", null, "Failed loading audio file with status: " + xhr.status + ".");
1739
+ return;
1740
+ }
1741
+ decodeAudioData(xhr.response, self);
1742
+ };
1743
+ xhr.onerror = function() {
1744
+ if (self._webAudio) {
1745
+ self._html5 = true;
1746
+ self._webAudio = false;
1747
+ self._sounds = [];
1748
+ delete cache[url];
1749
+ self.load();
1750
+ }
1751
+ };
1752
+ safeXhrSend(xhr);
1753
+ }
1754
+ };
1755
+ var safeXhrSend = function(xhr) {
1756
+ try {
1757
+ xhr.send();
1758
+ } catch (e) {
1759
+ xhr.onerror();
1760
+ }
1761
+ };
1762
+ var decodeAudioData = function(arraybuffer, self) {
1763
+ var error = function() {
1764
+ self._emit("loaderror", null, "Decoding audio data failed.");
1765
+ };
1766
+ var success = function(buffer) {
1767
+ if (buffer && self._sounds.length > 0) {
1768
+ cache[self._src] = buffer;
1769
+ loadSound(self, buffer);
1770
+ } else {
1771
+ error();
1772
+ }
1773
+ };
1774
+ if (typeof Promise !== "undefined" && Howler3.ctx.decodeAudioData.length === 1) {
1775
+ Howler3.ctx.decodeAudioData(arraybuffer).then(success).catch(error);
1776
+ } else {
1777
+ Howler3.ctx.decodeAudioData(arraybuffer, success, error);
1778
+ }
1779
+ };
1780
+ var loadSound = function(self, buffer) {
1781
+ if (buffer && !self._duration) {
1782
+ self._duration = buffer.duration;
1783
+ }
1784
+ if (Object.keys(self._sprite).length === 0) {
1785
+ self._sprite = { __default: [0, self._duration * 1e3] };
1786
+ }
1787
+ if (self._state !== "loaded") {
1788
+ self._state = "loaded";
1789
+ self._emit("load");
1790
+ self._loadQueue();
1791
+ }
1792
+ };
1793
+ var setupAudioContext = function() {
1794
+ if (!Howler3.usingWebAudio) {
1795
+ return;
1796
+ }
1797
+ try {
1798
+ if (typeof AudioContext !== "undefined") {
1799
+ Howler3.ctx = new AudioContext();
1800
+ } else if (typeof webkitAudioContext !== "undefined") {
1801
+ Howler3.ctx = new webkitAudioContext();
1802
+ } else {
1803
+ Howler3.usingWebAudio = false;
1804
+ }
1805
+ } catch (e) {
1806
+ Howler3.usingWebAudio = false;
1807
+ }
1808
+ if (!Howler3.ctx) {
1809
+ Howler3.usingWebAudio = false;
1810
+ }
1811
+ var iOS = /iP(hone|od|ad)/.test(Howler3._navigator && Howler3._navigator.platform);
1812
+ var appVersion = Howler3._navigator && Howler3._navigator.appVersion.match(/OS (\d+)_(\d+)_?(\d+)?/);
1813
+ var version = appVersion ? parseInt(appVersion[1], 10) : null;
1814
+ if (iOS && version && version < 9) {
1815
+ var safari = /safari/.test(Howler3._navigator && Howler3._navigator.userAgent.toLowerCase());
1816
+ if (Howler3._navigator && !safari) {
1817
+ Howler3.usingWebAudio = false;
1818
+ }
1819
+ }
1820
+ if (Howler3.usingWebAudio) {
1821
+ Howler3.masterGain = typeof Howler3.ctx.createGain === "undefined" ? Howler3.ctx.createGainNode() : Howler3.ctx.createGain();
1822
+ Howler3.masterGain.gain.setValueAtTime(Howler3._muted ? 0 : Howler3._volume, Howler3.ctx.currentTime);
1823
+ Howler3.masterGain.connect(Howler3.ctx.destination);
1824
+ }
1825
+ Howler3._setup();
1826
+ };
1827
+ if (typeof define === "function" && define.amd) {
1828
+ define([], function() {
1829
+ return {
1830
+ Howler: Howler3,
1831
+ Howl: Howl3
1832
+ };
1833
+ });
1834
+ }
1835
+ if (typeof exports2 !== "undefined") {
1836
+ exports2.Howler = Howler3;
1837
+ exports2.Howl = Howl3;
1838
+ }
1839
+ if (typeof global !== "undefined") {
1840
+ global.HowlerGlobal = HowlerGlobal2;
1841
+ global.Howler = Howler3;
1842
+ global.Howl = Howl3;
1843
+ global.Sound = Sound2;
1844
+ } else if (typeof window !== "undefined") {
1845
+ window.HowlerGlobal = HowlerGlobal2;
1846
+ window.Howler = Howler3;
1847
+ window.Howl = Howl3;
1848
+ window.Sound = Sound2;
1849
+ }
1850
+ })();
1851
+ (function() {
1852
+ "use strict";
1853
+ HowlerGlobal.prototype._pos = [0, 0, 0];
1854
+ HowlerGlobal.prototype._orientation = [0, 0, -1, 0, 1, 0];
1855
+ HowlerGlobal.prototype.stereo = function(pan) {
1856
+ var self = this;
1857
+ if (!self.ctx || !self.ctx.listener) {
1858
+ return self;
1859
+ }
1860
+ for (var i = self._howls.length - 1; i >= 0; i--) {
1861
+ self._howls[i].stereo(pan);
1862
+ }
1863
+ return self;
1864
+ };
1865
+ HowlerGlobal.prototype.pos = function(x, y, z) {
1866
+ var self = this;
1867
+ if (!self.ctx || !self.ctx.listener) {
1868
+ return self;
1869
+ }
1870
+ y = typeof y !== "number" ? self._pos[1] : y;
1871
+ z = typeof z !== "number" ? self._pos[2] : z;
1872
+ if (typeof x === "number") {
1873
+ self._pos = [x, y, z];
1874
+ if (typeof self.ctx.listener.positionX !== "undefined") {
1875
+ self.ctx.listener.positionX.setTargetAtTime(self._pos[0], Howler.ctx.currentTime, 0.1);
1876
+ self.ctx.listener.positionY.setTargetAtTime(self._pos[1], Howler.ctx.currentTime, 0.1);
1877
+ self.ctx.listener.positionZ.setTargetAtTime(self._pos[2], Howler.ctx.currentTime, 0.1);
1878
+ } else {
1879
+ self.ctx.listener.setPosition(self._pos[0], self._pos[1], self._pos[2]);
1880
+ }
1881
+ } else {
1882
+ return self._pos;
1883
+ }
1884
+ return self;
1885
+ };
1886
+ HowlerGlobal.prototype.orientation = function(x, y, z, xUp, yUp, zUp) {
1887
+ var self = this;
1888
+ if (!self.ctx || !self.ctx.listener) {
1889
+ return self;
1890
+ }
1891
+ var or = self._orientation;
1892
+ y = typeof y !== "number" ? or[1] : y;
1893
+ z = typeof z !== "number" ? or[2] : z;
1894
+ xUp = typeof xUp !== "number" ? or[3] : xUp;
1895
+ yUp = typeof yUp !== "number" ? or[4] : yUp;
1896
+ zUp = typeof zUp !== "number" ? or[5] : zUp;
1897
+ if (typeof x === "number") {
1898
+ self._orientation = [x, y, z, xUp, yUp, zUp];
1899
+ if (typeof self.ctx.listener.forwardX !== "undefined") {
1900
+ self.ctx.listener.forwardX.setTargetAtTime(x, Howler.ctx.currentTime, 0.1);
1901
+ self.ctx.listener.forwardY.setTargetAtTime(y, Howler.ctx.currentTime, 0.1);
1902
+ self.ctx.listener.forwardZ.setTargetAtTime(z, Howler.ctx.currentTime, 0.1);
1903
+ self.ctx.listener.upX.setTargetAtTime(xUp, Howler.ctx.currentTime, 0.1);
1904
+ self.ctx.listener.upY.setTargetAtTime(yUp, Howler.ctx.currentTime, 0.1);
1905
+ self.ctx.listener.upZ.setTargetAtTime(zUp, Howler.ctx.currentTime, 0.1);
1906
+ } else {
1907
+ self.ctx.listener.setOrientation(x, y, z, xUp, yUp, zUp);
1908
+ }
1909
+ } else {
1910
+ return or;
1911
+ }
1912
+ return self;
1913
+ };
1914
+ Howl.prototype.init = /* @__PURE__ */ (function(_super) {
1915
+ return function(o) {
1916
+ var self = this;
1917
+ self._orientation = o.orientation || [1, 0, 0];
1918
+ self._stereo = o.stereo || null;
1919
+ self._pos = o.pos || null;
1920
+ self._pannerAttr = {
1921
+ coneInnerAngle: typeof o.coneInnerAngle !== "undefined" ? o.coneInnerAngle : 360,
1922
+ coneOuterAngle: typeof o.coneOuterAngle !== "undefined" ? o.coneOuterAngle : 360,
1923
+ coneOuterGain: typeof o.coneOuterGain !== "undefined" ? o.coneOuterGain : 0,
1924
+ distanceModel: typeof o.distanceModel !== "undefined" ? o.distanceModel : "inverse",
1925
+ maxDistance: typeof o.maxDistance !== "undefined" ? o.maxDistance : 1e4,
1926
+ panningModel: typeof o.panningModel !== "undefined" ? o.panningModel : "HRTF",
1927
+ refDistance: typeof o.refDistance !== "undefined" ? o.refDistance : 1,
1928
+ rolloffFactor: typeof o.rolloffFactor !== "undefined" ? o.rolloffFactor : 1
1929
+ };
1930
+ self._onstereo = o.onstereo ? [{ fn: o.onstereo }] : [];
1931
+ self._onpos = o.onpos ? [{ fn: o.onpos }] : [];
1932
+ self._onorientation = o.onorientation ? [{ fn: o.onorientation }] : [];
1933
+ return _super.call(this, o);
1934
+ };
1935
+ })(Howl.prototype.init);
1936
+ Howl.prototype.stereo = function(pan, id) {
1937
+ var self = this;
1938
+ if (!self._webAudio) {
1939
+ return self;
1940
+ }
1941
+ if (self._state !== "loaded") {
1942
+ self._queue.push({
1943
+ event: "stereo",
1944
+ action: function() {
1945
+ self.stereo(pan, id);
1946
+ }
1947
+ });
1948
+ return self;
1949
+ }
1950
+ var pannerType = typeof Howler.ctx.createStereoPanner === "undefined" ? "spatial" : "stereo";
1951
+ if (typeof id === "undefined") {
1952
+ if (typeof pan === "number") {
1953
+ self._stereo = pan;
1954
+ self._pos = [pan, 0, 0];
1955
+ } else {
1956
+ return self._stereo;
1957
+ }
1958
+ }
1959
+ var ids = self._getSoundIds(id);
1960
+ for (var i = 0; i < ids.length; i++) {
1961
+ var sound = self._soundById(ids[i]);
1962
+ if (sound) {
1963
+ if (typeof pan === "number") {
1964
+ sound._stereo = pan;
1965
+ sound._pos = [pan, 0, 0];
1966
+ if (sound._node) {
1967
+ sound._pannerAttr.panningModel = "equalpower";
1968
+ if (!sound._panner || !sound._panner.pan) {
1969
+ setupPanner(sound, pannerType);
1970
+ }
1971
+ if (pannerType === "spatial") {
1972
+ if (typeof sound._panner.positionX !== "undefined") {
1973
+ sound._panner.positionX.setValueAtTime(pan, Howler.ctx.currentTime);
1974
+ sound._panner.positionY.setValueAtTime(0, Howler.ctx.currentTime);
1975
+ sound._panner.positionZ.setValueAtTime(0, Howler.ctx.currentTime);
1976
+ } else {
1977
+ sound._panner.setPosition(pan, 0, 0);
1978
+ }
1979
+ } else {
1980
+ sound._panner.pan.setValueAtTime(pan, Howler.ctx.currentTime);
1981
+ }
1982
+ }
1983
+ self._emit("stereo", sound._id);
1984
+ } else {
1985
+ return sound._stereo;
1986
+ }
1987
+ }
1988
+ }
1989
+ return self;
1990
+ };
1991
+ Howl.prototype.pos = function(x, y, z, id) {
1992
+ var self = this;
1993
+ if (!self._webAudio) {
1994
+ return self;
1995
+ }
1996
+ if (self._state !== "loaded") {
1997
+ self._queue.push({
1998
+ event: "pos",
1999
+ action: function() {
2000
+ self.pos(x, y, z, id);
2001
+ }
2002
+ });
2003
+ return self;
2004
+ }
2005
+ y = typeof y !== "number" ? 0 : y;
2006
+ z = typeof z !== "number" ? -0.5 : z;
2007
+ if (typeof id === "undefined") {
2008
+ if (typeof x === "number") {
2009
+ self._pos = [x, y, z];
2010
+ } else {
2011
+ return self._pos;
2012
+ }
2013
+ }
2014
+ var ids = self._getSoundIds(id);
2015
+ for (var i = 0; i < ids.length; i++) {
2016
+ var sound = self._soundById(ids[i]);
2017
+ if (sound) {
2018
+ if (typeof x === "number") {
2019
+ sound._pos = [x, y, z];
2020
+ if (sound._node) {
2021
+ if (!sound._panner || sound._panner.pan) {
2022
+ setupPanner(sound, "spatial");
2023
+ }
2024
+ if (typeof sound._panner.positionX !== "undefined") {
2025
+ sound._panner.positionX.setValueAtTime(x, Howler.ctx.currentTime);
2026
+ sound._panner.positionY.setValueAtTime(y, Howler.ctx.currentTime);
2027
+ sound._panner.positionZ.setValueAtTime(z, Howler.ctx.currentTime);
2028
+ } else {
2029
+ sound._panner.setPosition(x, y, z);
2030
+ }
2031
+ }
2032
+ self._emit("pos", sound._id);
2033
+ } else {
2034
+ return sound._pos;
2035
+ }
2036
+ }
2037
+ }
2038
+ return self;
2039
+ };
2040
+ Howl.prototype.orientation = function(x, y, z, id) {
2041
+ var self = this;
2042
+ if (!self._webAudio) {
2043
+ return self;
2044
+ }
2045
+ if (self._state !== "loaded") {
2046
+ self._queue.push({
2047
+ event: "orientation",
2048
+ action: function() {
2049
+ self.orientation(x, y, z, id);
2050
+ }
2051
+ });
2052
+ return self;
2053
+ }
2054
+ y = typeof y !== "number" ? self._orientation[1] : y;
2055
+ z = typeof z !== "number" ? self._orientation[2] : z;
2056
+ if (typeof id === "undefined") {
2057
+ if (typeof x === "number") {
2058
+ self._orientation = [x, y, z];
2059
+ } else {
2060
+ return self._orientation;
2061
+ }
2062
+ }
2063
+ var ids = self._getSoundIds(id);
2064
+ for (var i = 0; i < ids.length; i++) {
2065
+ var sound = self._soundById(ids[i]);
2066
+ if (sound) {
2067
+ if (typeof x === "number") {
2068
+ sound._orientation = [x, y, z];
2069
+ if (sound._node) {
2070
+ if (!sound._panner) {
2071
+ if (!sound._pos) {
2072
+ sound._pos = self._pos || [0, 0, -0.5];
2073
+ }
2074
+ setupPanner(sound, "spatial");
2075
+ }
2076
+ if (typeof sound._panner.orientationX !== "undefined") {
2077
+ sound._panner.orientationX.setValueAtTime(x, Howler.ctx.currentTime);
2078
+ sound._panner.orientationY.setValueAtTime(y, Howler.ctx.currentTime);
2079
+ sound._panner.orientationZ.setValueAtTime(z, Howler.ctx.currentTime);
2080
+ } else {
2081
+ sound._panner.setOrientation(x, y, z);
2082
+ }
2083
+ }
2084
+ self._emit("orientation", sound._id);
2085
+ } else {
2086
+ return sound._orientation;
2087
+ }
2088
+ }
2089
+ }
2090
+ return self;
2091
+ };
2092
+ Howl.prototype.pannerAttr = function() {
2093
+ var self = this;
2094
+ var args = arguments;
2095
+ var o, id, sound;
2096
+ if (!self._webAudio) {
2097
+ return self;
2098
+ }
2099
+ if (args.length === 0) {
2100
+ return self._pannerAttr;
2101
+ } else if (args.length === 1) {
2102
+ if (typeof args[0] === "object") {
2103
+ o = args[0];
2104
+ if (typeof id === "undefined") {
2105
+ if (!o.pannerAttr) {
2106
+ o.pannerAttr = {
2107
+ coneInnerAngle: o.coneInnerAngle,
2108
+ coneOuterAngle: o.coneOuterAngle,
2109
+ coneOuterGain: o.coneOuterGain,
2110
+ distanceModel: o.distanceModel,
2111
+ maxDistance: o.maxDistance,
2112
+ refDistance: o.refDistance,
2113
+ rolloffFactor: o.rolloffFactor,
2114
+ panningModel: o.panningModel
2115
+ };
2116
+ }
2117
+ self._pannerAttr = {
2118
+ coneInnerAngle: typeof o.pannerAttr.coneInnerAngle !== "undefined" ? o.pannerAttr.coneInnerAngle : self._coneInnerAngle,
2119
+ coneOuterAngle: typeof o.pannerAttr.coneOuterAngle !== "undefined" ? o.pannerAttr.coneOuterAngle : self._coneOuterAngle,
2120
+ coneOuterGain: typeof o.pannerAttr.coneOuterGain !== "undefined" ? o.pannerAttr.coneOuterGain : self._coneOuterGain,
2121
+ distanceModel: typeof o.pannerAttr.distanceModel !== "undefined" ? o.pannerAttr.distanceModel : self._distanceModel,
2122
+ maxDistance: typeof o.pannerAttr.maxDistance !== "undefined" ? o.pannerAttr.maxDistance : self._maxDistance,
2123
+ refDistance: typeof o.pannerAttr.refDistance !== "undefined" ? o.pannerAttr.refDistance : self._refDistance,
2124
+ rolloffFactor: typeof o.pannerAttr.rolloffFactor !== "undefined" ? o.pannerAttr.rolloffFactor : self._rolloffFactor,
2125
+ panningModel: typeof o.pannerAttr.panningModel !== "undefined" ? o.pannerAttr.panningModel : self._panningModel
2126
+ };
2127
+ }
2128
+ } else {
2129
+ sound = self._soundById(parseInt(args[0], 10));
2130
+ return sound ? sound._pannerAttr : self._pannerAttr;
2131
+ }
2132
+ } else if (args.length === 2) {
2133
+ o = args[0];
2134
+ id = parseInt(args[1], 10);
2135
+ }
2136
+ var ids = self._getSoundIds(id);
2137
+ for (var i = 0; i < ids.length; i++) {
2138
+ sound = self._soundById(ids[i]);
2139
+ if (sound) {
2140
+ var pa = sound._pannerAttr;
2141
+ pa = {
2142
+ coneInnerAngle: typeof o.coneInnerAngle !== "undefined" ? o.coneInnerAngle : pa.coneInnerAngle,
2143
+ coneOuterAngle: typeof o.coneOuterAngle !== "undefined" ? o.coneOuterAngle : pa.coneOuterAngle,
2144
+ coneOuterGain: typeof o.coneOuterGain !== "undefined" ? o.coneOuterGain : pa.coneOuterGain,
2145
+ distanceModel: typeof o.distanceModel !== "undefined" ? o.distanceModel : pa.distanceModel,
2146
+ maxDistance: typeof o.maxDistance !== "undefined" ? o.maxDistance : pa.maxDistance,
2147
+ refDistance: typeof o.refDistance !== "undefined" ? o.refDistance : pa.refDistance,
2148
+ rolloffFactor: typeof o.rolloffFactor !== "undefined" ? o.rolloffFactor : pa.rolloffFactor,
2149
+ panningModel: typeof o.panningModel !== "undefined" ? o.panningModel : pa.panningModel
2150
+ };
2151
+ var panner = sound._panner;
2152
+ if (!panner) {
2153
+ if (!sound._pos) {
2154
+ sound._pos = self._pos || [0, 0, -0.5];
2155
+ }
2156
+ setupPanner(sound, "spatial");
2157
+ panner = sound._panner;
2158
+ }
2159
+ panner.coneInnerAngle = pa.coneInnerAngle;
2160
+ panner.coneOuterAngle = pa.coneOuterAngle;
2161
+ panner.coneOuterGain = pa.coneOuterGain;
2162
+ panner.distanceModel = pa.distanceModel;
2163
+ panner.maxDistance = pa.maxDistance;
2164
+ panner.refDistance = pa.refDistance;
2165
+ panner.rolloffFactor = pa.rolloffFactor;
2166
+ panner.panningModel = pa.panningModel;
2167
+ }
2168
+ }
2169
+ return self;
2170
+ };
2171
+ Sound.prototype.init = /* @__PURE__ */ (function(_super) {
2172
+ return function() {
2173
+ var self = this;
2174
+ var parent = self._parent;
2175
+ self._orientation = parent._orientation;
2176
+ self._stereo = parent._stereo;
2177
+ self._pos = parent._pos;
2178
+ self._pannerAttr = parent._pannerAttr;
2179
+ _super.call(this);
2180
+ if (self._stereo) {
2181
+ parent.stereo(self._stereo);
2182
+ } else if (self._pos) {
2183
+ parent.pos(self._pos[0], self._pos[1], self._pos[2], self._id);
2184
+ }
2185
+ };
2186
+ })(Sound.prototype.init);
2187
+ Sound.prototype.reset = /* @__PURE__ */ (function(_super) {
2188
+ return function() {
2189
+ var self = this;
2190
+ var parent = self._parent;
2191
+ self._orientation = parent._orientation;
2192
+ self._stereo = parent._stereo;
2193
+ self._pos = parent._pos;
2194
+ self._pannerAttr = parent._pannerAttr;
2195
+ if (self._stereo) {
2196
+ parent.stereo(self._stereo);
2197
+ } else if (self._pos) {
2198
+ parent.pos(self._pos[0], self._pos[1], self._pos[2], self._id);
2199
+ } else if (self._panner) {
2200
+ self._panner.disconnect(0);
2201
+ self._panner = void 0;
2202
+ parent._refreshBuffer(self);
2203
+ }
2204
+ return _super.call(this);
2205
+ };
2206
+ })(Sound.prototype.reset);
2207
+ var setupPanner = function(sound, type) {
2208
+ type = type || "spatial";
2209
+ if (type === "spatial") {
2210
+ sound._panner = Howler.ctx.createPanner();
2211
+ sound._panner.coneInnerAngle = sound._pannerAttr.coneInnerAngle;
2212
+ sound._panner.coneOuterAngle = sound._pannerAttr.coneOuterAngle;
2213
+ sound._panner.coneOuterGain = sound._pannerAttr.coneOuterGain;
2214
+ sound._panner.distanceModel = sound._pannerAttr.distanceModel;
2215
+ sound._panner.maxDistance = sound._pannerAttr.maxDistance;
2216
+ sound._panner.refDistance = sound._pannerAttr.refDistance;
2217
+ sound._panner.rolloffFactor = sound._pannerAttr.rolloffFactor;
2218
+ sound._panner.panningModel = sound._pannerAttr.panningModel;
2219
+ if (typeof sound._panner.positionX !== "undefined") {
2220
+ sound._panner.positionX.setValueAtTime(sound._pos[0], Howler.ctx.currentTime);
2221
+ sound._panner.positionY.setValueAtTime(sound._pos[1], Howler.ctx.currentTime);
2222
+ sound._panner.positionZ.setValueAtTime(sound._pos[2], Howler.ctx.currentTime);
2223
+ } else {
2224
+ sound._panner.setPosition(sound._pos[0], sound._pos[1], sound._pos[2]);
2225
+ }
2226
+ if (typeof sound._panner.orientationX !== "undefined") {
2227
+ sound._panner.orientationX.setValueAtTime(sound._orientation[0], Howler.ctx.currentTime);
2228
+ sound._panner.orientationY.setValueAtTime(sound._orientation[1], Howler.ctx.currentTime);
2229
+ sound._panner.orientationZ.setValueAtTime(sound._orientation[2], Howler.ctx.currentTime);
2230
+ } else {
2231
+ sound._panner.setOrientation(sound._orientation[0], sound._orientation[1], sound._orientation[2]);
2232
+ }
2233
+ } else {
2234
+ sound._panner = Howler.ctx.createStereoPanner();
2235
+ sound._panner.pan.setValueAtTime(sound._stereo, Howler.ctx.currentTime);
2236
+ }
2237
+ sound._panner.connect(sound._node);
2238
+ if (!sound._paused) {
2239
+ sound._parent.pause(sound._id, true).play(sound._id, true);
2240
+ }
2241
+ };
2242
+ })();
2243
+ }
2244
+ });
2245
+
2246
+ // src/index.ts
2247
+ var index_exports = {};
2248
+ __export(index_exports, {
2249
+ VERSION: () => VERSION,
2250
+ createStarAudio: () => createStarAudio,
2251
+ default: () => index_default
2252
+ });
2253
+ module.exports = __toCommonJS(index_exports);
2254
+
2255
+ // src/internals/StarAudioImpl.ts
2256
+ var import_howler = __toESM(require_howler(), 1);
2257
+
2258
+ // src/internals/ProceduralSynth.ts
2259
+ var PROCEDURAL_VOLUME_SCALE = 0.03;
2260
+ var PRESET_VERSION = "v34";
2261
+ var PRESET_DEFINITIONS = {
2262
+ beep: {
2263
+ waveform: "square",
2264
+ // More presence than sine
2265
+ frequency: [660, 880],
2266
+ // E5-A5 - friendly "boop boop"
2267
+ duration: 0.1,
2268
+ // Two-tone button press
2269
+ volume: 0.38,
2270
+ // Clear confirmation
2271
+ envelope: "percussive"
2272
+ },
2273
+ coin: {
2274
+ waveform: "sine",
2275
+ // Pure, bell-like "ting"
2276
+ frequency: [988, 1318, 1568, 1976],
2277
+ // B5-E6-G6-B6 - shimmery ascending
2278
+ duration: 0.16,
2279
+ // Quick but lets the shimmer breathe
2280
+ volume: 0.42,
2281
+ // Rewarding presence
2282
+ envelope: "percussive"
2283
+ },
2284
+ pickup: {
2285
+ waveform: "sine",
2286
+ // Pure, satisfying
2287
+ frequency: [784, 988, 1175, 1397],
2288
+ // G-B-D-F# - bright rising
2289
+ duration: 0.13,
2290
+ // Quick but complete
2291
+ volume: 0.4,
2292
+ // Satisfying positive feedback
2293
+ envelope: "percussive"
2294
+ },
2295
+ jump: {
2296
+ waveform: "triangle",
2297
+ // Warmer, less harsh than square
2298
+ frequency: [180, 240, 320, 450],
2299
+ // Quick rising arpeggio simulates "boing"
2300
+ duration: 0.15,
2301
+ // Tighter for snappier feel
2302
+ volume: 0.7,
2303
+ // 2x for satisfying bounce feedback
2304
+ envelope: "percussive"
2305
+ },
2306
+ hurt: {
2307
+ waveform: "sawtooth",
2308
+ // Harsh, painful quality
2309
+ frequency: [600, 400, 250, 150],
2310
+ // Sharp descending sweep - "oof!"
2311
+ duration: 0.25,
2312
+ // Quick but noticeable
2313
+ volume: 0.28,
2314
+ // Slightly more presence for feedback
2315
+ envelope: "percussive"
2316
+ },
2317
+ explosion: {
2318
+ waveform: "sawtooth",
2319
+ // Harsh, noise-like
2320
+ frequency: [
2321
+ 800,
2322
+ 600,
2323
+ 450,
2324
+ 320,
2325
+ 220,
2326
+ // Initial crack/burst (fast descent)
2327
+ 160,
2328
+ 120,
2329
+ 90,
2330
+ 70,
2331
+ 55,
2332
+ // Transition to rumble
2333
+ 45,
2334
+ 40,
2335
+ 35,
2336
+ 30
2337
+ // Deep rumble tail
2338
+ ],
2339
+ // Dense frequency sweep simulates noise
2340
+ duration: 0.65,
2341
+ // Long tail for satisfying boom
2342
+ volume: 0.45,
2343
+ // Needs big presence
2344
+ envelope: "percussive"
2345
+ },
2346
+ powerup: {
2347
+ waveform: "triangle",
2348
+ // Warm, rich
2349
+ frequency: [262, 330, 392, 523, 659, 784, 1046],
2350
+ // C-E-G-C-E-G-C - full ascending cascade
2351
+ duration: 0.48,
2352
+ // Long celebration
2353
+ volume: 0.44,
2354
+ // Epic presence
2355
+ envelope: "sustained"
2356
+ },
2357
+ shoot: {
2358
+ waveform: "triangle",
2359
+ // Versatile - works for guns, bows, throws, spells
2360
+ frequency: [
2361
+ 1800,
2362
+ 1400,
2363
+ // Quick "release" attack (projectile launch feel)
2364
+ 900,
2365
+ 600,
2366
+ // Mid-range arc (satisfying descent)
2367
+ 380,
2368
+ 220
2369
+ // Subtle bass anchor (adds weight without being heavy)
2370
+ ],
2371
+ // Universal projectile: works for bullets, arrows, fireballs, thrown items
2372
+ duration: 0.1,
2373
+ // Fast - perfect for rapid fire
2374
+ volume: 0.45,
2375
+ // Present but not overpowering
2376
+ envelope: "percussive"
2377
+ },
2378
+ laser: {
2379
+ waveform: "sawtooth",
2380
+ // Rich harmonics for that sci-fi "zing" bite
2381
+ frequency: [
2382
+ 3200,
2383
+ 2800,
2384
+ 2400,
2385
+ // Powerful charge-up without the shrillness
2386
+ 1800,
2387
+ 1400,
2388
+ 1e3,
2389
+ // Mid-range energy sweep (moderate)
2390
+ 700,
2391
+ 450,
2392
+ 260,
2393
+ 140
2394
+ // Bass impact tail (satisfying thump)
2395
+ ],
2396
+ // Valve-quality energy weapon: smooth attack → powerful descent → bass punch
2397
+ duration: 0.18,
2398
+ // Slightly longer for the full sci-fi experience
2399
+ volume: 0.42,
2400
+ // Punchy presence - this is a WEAPON
2401
+ envelope: "percussive"
2402
+ },
2403
+ error: {
2404
+ waveform: "sawtooth",
2405
+ // Harsh, unpleasant (intentionally)
2406
+ frequency: [800, 500, 250, 150],
2407
+ // Sharp descending blast - "WRONG!"
2408
+ duration: 0.22,
2409
+ // Long enough to register as negative
2410
+ volume: 0.32,
2411
+ // Needs to be noticed
2412
+ envelope: "percussive"
2413
+ },
2414
+ click: {
2415
+ waveform: "sine",
2416
+ // Pure, pleasant
2417
+ frequency: 800,
2418
+ // Simple, clean tone
2419
+ duration: 0.05,
2420
+ // Quick
2421
+ volume: 0.7,
2422
+ // 2x previous (was 0.35)
2423
+ envelope: "percussive"
2424
+ },
2425
+ success: {
2426
+ waveform: "triangle",
2427
+ // Warm, full
2428
+ frequency: [
2429
+ 784,
2430
+ 988,
2431
+ 1175,
2432
+ // G-B-D (ascending excitement - "dun dun dun")
2433
+ 1046,
2434
+ 1318,
2435
+ 1568,
2436
+ 1976,
2437
+ // C-E-G-B (soaring triumphant ascent)
2438
+ 2349
2439
+ // D-D-D-D (sustained victorious peak)
2440
+ ],
2441
+ // Victory phrase with sustained high note at end
2442
+ duration: 1.2,
2443
+ // Extended for longer sustained finale
2444
+ volume: 0.48,
2445
+ // Victorious presence
2446
+ envelope: "sustained"
2447
+ },
2448
+ bonus: {
2449
+ waveform: "sine",
2450
+ // Pure, magical sparkle
2451
+ frequency: [659, 880, 1046, 1318, 1568, 1976],
2452
+ // E-A-C-E-G-B - soaring ascent
2453
+ duration: 0.42,
2454
+ // Longer celebration
2455
+ volume: 0.5,
2456
+ // BIGGEST reward - most epic sound
2457
+ envelope: "sustained"
2458
+ },
2459
+ select: {
2460
+ waveform: "sine",
2461
+ // Pure, pleasant - not annoying when repeated
2462
+ frequency: [880, 1046],
2463
+ // A-C - gentle upward confirmation
2464
+ duration: 0.06,
2465
+ // Very quick - instant responsive feedback
2466
+ volume: 0.35,
2467
+ // Subtle - plays constantly in menus
2468
+ envelope: "percussive"
2469
+ },
2470
+ unlock: {
2471
+ waveform: "triangle",
2472
+ // Warm, satisfying
2473
+ frequency: [
2474
+ 220,
2475
+ 196,
2476
+ 220,
2477
+ // Low "ka-chunk" mechanism (back and forth)
2478
+ 440,
2479
+ 659,
2480
+ 880,
2481
+ 1175
2482
+ // Rising victorious chime (A-E-A-D)
2483
+ ],
2484
+ // Two-phase: mechanical lock → triumphant unlock
2485
+ duration: 0.35,
2486
+ // Long enough to savor the satisfaction
2487
+ volume: 0.46,
2488
+ // Strong presence - this is a reward moment
2489
+ envelope: "sustained"
2490
+ },
2491
+ swoosh: {
2492
+ waveform: "sine",
2493
+ // Pure, soft whoosh
2494
+ frequency: [
2495
+ 350,
2496
+ 600,
2497
+ 900,
2498
+ 1100,
2499
+ // Gentle rise (approaching)
2500
+ 950,
2501
+ 700,
2502
+ 450,
2503
+ 250
2504
+ // Smooth fall (passing by)
2505
+ ],
2506
+ // Soft doppler: gentle motion arc
2507
+ duration: 0.15,
2508
+ // Smooth, unhurried
2509
+ volume: 0.36,
2510
+ // Softer - background motion
2511
+ envelope: "percussive"
2512
+ },
2513
+ hit: {
2514
+ waveform: "sawtooth",
2515
+ // Dense harmonics for impact texture
2516
+ frequency: [
2517
+ 450,
2518
+ 400,
2519
+ 350,
2520
+ // Tight mid-range cluster (main impact body)
2521
+ 300,
2522
+ 250,
2523
+ 200,
2524
+ // Lower cluster (weight/thump)
2525
+ 280,
2526
+ 340,
2527
+ 420
2528
+ // Quick rising "ting" (satisfaction/success)
2529
+ ],
2530
+ // Impact thump + rising satisfaction (you landed it!)
2531
+ duration: 0.06,
2532
+ // Slightly longer for satisfaction tail
2533
+ volume: 0.56,
2534
+ // Strong and rewarding
2535
+ envelope: "percussive"
2536
+ }
2537
+ };
2538
+ function isPreset(value) {
2539
+ return typeof value === "string" && value in PRESET_DEFINITIONS;
2540
+ }
2541
+ function isSynthDefinition(value) {
2542
+ return typeof value === "object" && value !== null && !Array.isArray(value) && ("waveform" in value || "frequency" in value || "duration" in value);
2543
+ }
2544
+ async function generateWavUrl(definition) {
2545
+ const {
2546
+ waveform = "square",
2547
+ frequency = 440,
2548
+ duration = 0.1,
2549
+ volume: presetVolume = 1,
2550
+ envelope = "percussive"
2551
+ } = definition;
2552
+ const sampleRate = 48e3;
2553
+ const numSamples = Math.floor(sampleRate * duration);
2554
+ const offlineCtx = new OfflineAudioContext(1, numSamples, sampleRate);
2555
+ const frequencies = Array.isArray(frequency) ? frequency : [frequency];
2556
+ const gainNode = offlineCtx.createGain();
2557
+ gainNode.connect(offlineCtx.destination);
2558
+ const scaledVolume = presetVolume * PROCEDURAL_VOLUME_SCALE;
2559
+ const attackTime = envelope === "percussive" ? 0.01 : 0.05;
2560
+ const releaseTime = envelope === "percussive" ? 0.05 : 0.1;
2561
+ gainNode.gain.setValueAtTime(0, 0);
2562
+ gainNode.gain.linearRampToValueAtTime(scaledVolume, attackTime);
2563
+ gainNode.gain.linearRampToValueAtTime(scaledVolume * 0.7, duration - releaseTime);
2564
+ gainNode.gain.linearRampToValueAtTime(0, duration);
2565
+ if (frequencies.length === 1) {
2566
+ const osc = offlineCtx.createOscillator();
2567
+ osc.type = waveform;
2568
+ osc.frequency.setValueAtTime(frequencies[0], 0);
2569
+ osc.connect(gainNode);
2570
+ osc.start(0);
2571
+ osc.stop(duration);
2572
+ } else {
2573
+ const timePerNote = duration / frequencies.length;
2574
+ frequencies.forEach((freq, index) => {
2575
+ const osc = offlineCtx.createOscillator();
2576
+ osc.type = waveform;
2577
+ const startTime = index * timePerNote;
2578
+ const endTime = startTime + timePerNote;
2579
+ osc.frequency.setValueAtTime(freq, startTime);
2580
+ osc.connect(gainNode);
2581
+ osc.start(startTime);
2582
+ osc.stop(endTime);
2583
+ });
2584
+ }
2585
+ const audioBuffer = await offlineCtx.startRendering();
2586
+ const wavBlob = audioBufferToWav(audioBuffer);
2587
+ return URL.createObjectURL(wavBlob);
2588
+ }
2589
+ function audioBufferToWav(buffer) {
2590
+ const numChannels = buffer.numberOfChannels;
2591
+ const sampleRate = buffer.sampleRate;
2592
+ const format = 1;
2593
+ const bitDepth = 16;
2594
+ const bytesPerSample = bitDepth / 8;
2595
+ const blockAlign = numChannels * bytesPerSample;
2596
+ const data = buffer.getChannelData(0);
2597
+ const dataLength = data.length * bytesPerSample;
2598
+ const bufferLength = 44 + dataLength;
2599
+ const arrayBuffer = new ArrayBuffer(bufferLength);
2600
+ const view = new DataView(arrayBuffer);
2601
+ writeString(view, 0, "RIFF");
2602
+ view.setUint32(4, 36 + dataLength, true);
2603
+ writeString(view, 8, "WAVE");
2604
+ writeString(view, 12, "fmt ");
2605
+ view.setUint32(16, 16, true);
2606
+ view.setUint16(20, format, true);
2607
+ view.setUint16(22, numChannels, true);
2608
+ view.setUint32(24, sampleRate, true);
2609
+ view.setUint32(28, sampleRate * blockAlign, true);
2610
+ view.setUint16(32, blockAlign, true);
2611
+ view.setUint16(34, bitDepth, true);
2612
+ writeString(view, 36, "data");
2613
+ view.setUint32(40, dataLength, true);
2614
+ let offset = 44;
2615
+ for (let i = 0; i < data.length; i++) {
2616
+ const sample = Math.max(-1, Math.min(1, data[i]));
2617
+ const int16 = sample < 0 ? sample * 32768 : sample * 32767;
2618
+ view.setInt16(offset, int16, true);
2619
+ offset += 2;
2620
+ }
2621
+ return new Blob([arrayBuffer], { type: "audio/wav" });
2622
+ }
2623
+ function writeString(view, offset, string) {
2624
+ for (let i = 0; i < string.length; i++) {
2625
+ view.setUint8(offset + i, string.charCodeAt(i));
2626
+ }
2627
+ }
2628
+ function resolveDefinition(value) {
2629
+ if (isPreset(value)) {
2630
+ return { ...PRESET_DEFINITIONS[value], _version: PRESET_VERSION };
2631
+ }
2632
+ return value;
2633
+ }
2634
+
2635
+ // src/internals/StarAudioImpl.ts
2636
+ var SoundHandleImpl = class {
2637
+ constructor(id, _howl, _soundId) {
2638
+ this.id = id;
2639
+ this._howl = _howl;
2640
+ this._soundId = _soundId;
2641
+ }
2642
+ get playing() {
2643
+ return this._howl.playing(this._soundId);
2644
+ }
2645
+ stop(at = 0) {
2646
+ this._howl.stop(this._soundId);
2647
+ }
2648
+ setVolume(v, _nowPlusSec = 0) {
2649
+ const clampedVolume = Math.max(0, Math.min(1, v));
2650
+ this._howl.volume(clampedVolume, this._soundId);
2651
+ }
2652
+ };
2653
+ var StarAudioImpl = class {
2654
+ constructor(options = {}) {
2655
+ this._state = "locked";
2656
+ this._sounds = /* @__PURE__ */ new Map();
2657
+ this._soundGroups = /* @__PURE__ */ new Map();
2658
+ this._soundVolumes = /* @__PURE__ */ new Map();
2659
+ // Per-sound volume overrides
2660
+ this._currentMusicId = null;
2661
+ this._currentMusicSoundId = null;
2662
+ this._unlockHandlerRemover = null;
2663
+ this._eventTarget = new EventTarget();
2664
+ this._isMuted = false;
2665
+ this._volumes = { music: 0.8, sfx: 0.9 };
2666
+ this._visibilityHandler = null;
2667
+ this._readyResolver = null;
2668
+ this._pendingFadeTimeout = null;
2669
+ this._generatedWavUrls = /* @__PURE__ */ new Map();
2670
+ this.playSound = this.play;
2671
+ this.setMusic = this.setMusicVolume;
2672
+ this.setSfx = this.setSfxVolume;
2673
+ this.mute = this.setMute;
2674
+ // --- Lifecycle ---
2675
+ this.pause = async () => {
2676
+ for (const howl of this._sounds.values()) {
2677
+ howl.pause();
2678
+ }
2679
+ this._state = "suspended";
2680
+ this._eventTarget.dispatchEvent(new Event("suspended"));
2681
+ };
2682
+ this.resume = async () => {
2683
+ if (this._state === "locked") {
2684
+ console.log("[StarAudio] Context will unlock on first user interaction");
2685
+ }
2686
+ if (this._currentMusicId && this._currentMusicSoundId !== null) {
2687
+ const musicHowl = this._sounds.get(this._currentMusicId);
2688
+ if (musicHowl && !musicHowl.playing(this._currentMusicSoundId)) {
2689
+ musicHowl.play(this._currentMusicSoundId);
2690
+ }
2691
+ }
2692
+ this._state = "running";
2693
+ this._eventTarget.dispatchEvent(new Event("resumed"));
2694
+ if (this._readyResolver) {
2695
+ this._readyResolver();
2696
+ this._readyResolver = null;
2697
+ }
2698
+ };
2699
+ this.unlock = this.resume;
2700
+ console.log(`[Star Audio v${VERSION}] Initializing...`);
2701
+ this._persistKey = options.persistKey ?? "star.audio.v0";
2702
+ this._loadState();
2703
+ if (options.initialMute !== void 0) {
2704
+ this._isMuted = options.initialMute;
2705
+ }
2706
+ if (options.initialVolumes) {
2707
+ if (options.initialVolumes.music !== void 0) {
2708
+ this._volumes.music = Math.max(0, Math.min(1, options.initialVolumes.music));
2709
+ }
2710
+ if (options.initialVolumes.sfx !== void 0) {
2711
+ this._volumes.sfx = Math.max(0, Math.min(1, options.initialVolumes.sfx));
2712
+ }
2713
+ }
2714
+ import_howler.Howler.volume(this._isMuted ? 0 : 1);
2715
+ this.ready = new Promise((resolve) => {
2716
+ this._readyResolver = resolve;
2717
+ if (import_howler.Howler.ctx && import_howler.Howler.ctx.state === "running") {
2718
+ this._state = "running";
2719
+ resolve();
2720
+ }
2721
+ });
2722
+ console.log("[StarAudio] SDK Initialized (powered by Howler.js)");
2723
+ if (options.unlockWith !== false) {
2724
+ this.attachUnlock(options.unlockWith === "auto" ? window : options.unlockWith);
2725
+ }
2726
+ if (options.suspendOnHidden ?? true) {
2727
+ this._visibilityHandler = () => {
2728
+ if (document.visibilityState === "hidden") {
2729
+ this.pause();
2730
+ } else if (document.visibilityState === "visible") {
2731
+ this.resume();
2732
+ }
2733
+ };
2734
+ document.addEventListener("visibilitychange", this._visibilityHandler);
2735
+ }
2736
+ this.music = {
2737
+ crossfadeTo: async (id, o = {}) => {
2738
+ const duration = (o.duration ?? 1) * 1e3;
2739
+ const loop = o.loop ?? true;
2740
+ const howl = this._sounds.get(id);
2741
+ if (!howl) {
2742
+ console.warn(`[StarAudio] Sound not loaded: ${id}`);
2743
+ return;
2744
+ }
2745
+ if (this._currentMusicId === id && this._currentMusicSoundId !== null) {
2746
+ console.log(`[StarAudio] Already playing ${id}, updating loop setting`);
2747
+ howl.loop(loop);
2748
+ return;
2749
+ }
2750
+ if (this._pendingFadeTimeout !== null) {
2751
+ clearTimeout(this._pendingFadeTimeout);
2752
+ this._pendingFadeTimeout = null;
2753
+ }
2754
+ if (this._currentMusicId && this._currentMusicSoundId !== null) {
2755
+ const currentHowl = this._sounds.get(this._currentMusicId);
2756
+ if (currentHowl) {
2757
+ const oldSoundId = this._currentMusicSoundId;
2758
+ currentHowl.fade(currentHowl.volume(), 0, duration, oldSoundId);
2759
+ this._pendingFadeTimeout = setTimeout(() => {
2760
+ currentHowl.stop(oldSoundId);
2761
+ this._pendingFadeTimeout = null;
2762
+ }, duration);
2763
+ }
2764
+ }
2765
+ const audioDuration = howl.duration();
2766
+ const safeToLoop = loop && audioDuration > 0.1;
2767
+ if (loop && !safeToLoop) {
2768
+ console.warn(`[StarAudio] \u26A0\uFE0F Audio "${id}" is too short (${audioDuration.toFixed(3)}s) to loop safely - disabling loop to prevent crash`);
2769
+ }
2770
+ howl.loop(safeToLoop);
2771
+ howl.volume(0);
2772
+ const soundId = howl.play();
2773
+ howl.fade(0, this._volumes.music, duration, soundId);
2774
+ this._currentMusicId = id;
2775
+ this._currentMusicSoundId = soundId;
2776
+ },
2777
+ stop: (fadeSec = 0.2) => {
2778
+ if (this._pendingFadeTimeout !== null) {
2779
+ clearTimeout(this._pendingFadeTimeout);
2780
+ this._pendingFadeTimeout = null;
2781
+ }
2782
+ if (this._currentMusicId && this._currentMusicSoundId !== null) {
2783
+ const howl = this._sounds.get(this._currentMusicId);
2784
+ if (howl) {
2785
+ const soundId = this._currentMusicSoundId;
2786
+ howl.fade(howl.volume(), 0, fadeSec * 1e3, soundId);
2787
+ this._pendingFadeTimeout = setTimeout(() => {
2788
+ howl.stop(soundId);
2789
+ this._currentMusicId = null;
2790
+ this._currentMusicSoundId = null;
2791
+ this._pendingFadeTimeout = null;
2792
+ }, fadeSec * 1e3);
2793
+ }
2794
+ }
2795
+ },
2796
+ fadeTo: void 0,
2797
+ switchTo: void 0
2798
+ };
2799
+ this.music.fadeTo = this.music.crossfadeTo;
2800
+ this.music.switchTo = this.music.crossfadeTo;
2801
+ this.musicFadeTo = this.music.crossfadeTo;
2802
+ }
2803
+ get state() {
2804
+ return this._state;
2805
+ }
2806
+ // --- Asset Loading ---
2807
+ async preload(manifest) {
2808
+ const promises = [];
2809
+ for (const [id, value] of Object.entries(manifest)) {
2810
+ if (isPreset(value) || isSynthDefinition(value)) {
2811
+ promises.push(this._loadProceduralSound(id, value));
2812
+ continue;
2813
+ }
2814
+ if (typeof value === "string" || Array.isArray(value)) {
2815
+ const strValue = Array.isArray(value) ? value[0] : value;
2816
+ if (typeof strValue === "string" && !strValue.includes(".") && !strValue.startsWith("http")) {
2817
+ console.error(`[StarAudio] \u26A0\uFE0F "${id}: '${strValue}'" looks like an invalid preset name. Valid presets: beep, click, select, jump, swoosh, shoot, laser, explosion, hit, hurt, coin, pickup, bonus, unlock, powerup, error, success. Use { synth: 'presetName' } for presets or provide a valid audio file path.`);
2818
+ continue;
2819
+ }
2820
+ promises.push(this.load({ id, src: value }));
2821
+ } else {
2822
+ const volume = "volume" in value ? value.volume : void 0;
2823
+ if (volume !== void 0) {
2824
+ this._soundVolumes.set(id, volume);
2825
+ }
2826
+ if ("synth" in value && value.synth) {
2827
+ promises.push(this._loadProceduralSound(id, value.synth, value.group));
2828
+ } else {
2829
+ promises.push(this.load({ id, ...value }));
2830
+ }
2831
+ }
2832
+ }
2833
+ const results = await Promise.allSettled(promises);
2834
+ const succeeded = results.filter((r) => r.status === "fulfilled").length;
2835
+ const failed = results.filter((r) => r.status === "rejected").length;
2836
+ if (failed > 0) {
2837
+ console.warn(`[StarAudio] Preload completed: ${succeeded} succeeded, ${failed} failed`);
2838
+ } else {
2839
+ console.log(`[StarAudio] Preload completed: ${succeeded} sounds loaded`);
2840
+ }
2841
+ }
2842
+ async _loadProceduralSound(id, value, group) {
2843
+ const definition = resolveDefinition(value);
2844
+ const defKey = typeof value === "string" ? `${value}:${JSON.stringify(definition)}` : JSON.stringify(definition);
2845
+ let wavUrl = this._generatedWavUrls.get(defKey);
2846
+ if (!wavUrl) {
2847
+ wavUrl = await generateWavUrl(definition);
2848
+ this._generatedWavUrls.set(defKey, wavUrl);
2849
+ }
2850
+ const inferredGroup = group || this._inferGroup(id);
2851
+ this._soundGroups.set(id, inferredGroup);
2852
+ return new Promise((resolve, reject) => {
2853
+ const howl = new import_howler.Howl({
2854
+ src: [wavUrl],
2855
+ format: ["wav"],
2856
+ // CRITICAL: Tell Howler.js this is a WAV file (blob URLs have no extension)
2857
+ volume: this._volumes[inferredGroup],
2858
+ html5: true,
2859
+ // Use HTML5 Audio for mobile reliability and silent mode bypass
2860
+ onload: () => {
2861
+ console.log(`[StarAudio] Generated procedural sound: ${id}`);
2862
+ this._sounds.set(id, howl);
2863
+ resolve();
2864
+ },
2865
+ onloaderror: (_id, error) => {
2866
+ console.warn(`[StarAudio] \u26A0\uFE0F Failed to generate procedural sound "${id}" - the game will continue without this sound.`, error);
2867
+ this._eventTarget.dispatchEvent(new Event("error"));
2868
+ this._soundGroups.delete(id);
2869
+ resolve();
2870
+ }
2871
+ });
2872
+ });
2873
+ }
2874
+ async load(config) {
2875
+ const { id, src, url, group } = config;
2876
+ const sources = src || url;
2877
+ if (!sources) {
2878
+ console.warn(`[StarAudio] No src/url provided for ${id}`);
2879
+ return;
2880
+ }
2881
+ const inferredGroup = group || this._inferGroup(id);
2882
+ this._soundGroups.set(id, inferredGroup);
2883
+ return new Promise((resolve, reject) => {
2884
+ const howl = new import_howler.Howl({
2885
+ src: Array.isArray(sources) ? sources : [sources],
2886
+ volume: this._volumes[inferredGroup],
2887
+ html5: true,
2888
+ // FORCE HTML5 AUDIO FOR MOBILE RELIABILITY
2889
+ onload: () => {
2890
+ console.log(`[StarAudio] Loaded: ${id}`);
2891
+ this._sounds.set(id, howl);
2892
+ resolve();
2893
+ },
2894
+ onloaderror: (_id, error) => {
2895
+ console.warn(`[StarAudio] \u26A0\uFE0F Failed to load "${id}" - the game will continue without this sound.`, error);
2896
+ this._eventTarget.dispatchEvent(new Event("error"));
2897
+ this._soundGroups.delete(id);
2898
+ resolve();
2899
+ }
2900
+ });
2901
+ });
2902
+ }
2903
+ // --- Playback ---
2904
+ play(id, opts = {}) {
2905
+ if (isPreset(id) && !this._sounds.has(id)) {
2906
+ console.info(`[StarAudio] Auto-generating preset: ${id}`);
2907
+ this._loadProceduralSound(id, id).catch((err) => {
2908
+ console.error(`[StarAudio] Failed to generate preset ${id}:`, err);
2909
+ });
2910
+ console.warn(`[StarAudio] Preset ${id} is generating, try again in a moment`);
2911
+ return null;
2912
+ }
2913
+ if ((opts.src || opts.url) && !this._sounds.has(id)) {
2914
+ console.log(`[StarAudio] Auto-loading: ${id}`);
2915
+ const sources = opts.src || opts.url;
2916
+ const inferredGroup = opts.group || this._inferGroup(id);
2917
+ this._soundGroups.set(id, inferredGroup);
2918
+ const requestedLoop = opts.loop ?? false;
2919
+ const howl2 = new import_howler.Howl({
2920
+ src: Array.isArray(sources) ? sources : [sources],
2921
+ volume: opts.volume ?? this._volumes[inferredGroup],
2922
+ loop: false,
2923
+ // Set to false initially, check duration in onload
2924
+ rate: opts.rate ?? 1,
2925
+ html5: true,
2926
+ autoplay: true,
2927
+ // Auto-play as soon as loaded
2928
+ onload: () => {
2929
+ console.log(`[StarAudio] Loaded and playing: ${id}`);
2930
+ if (requestedLoop) {
2931
+ const audioDuration = howl2.duration();
2932
+ const safeToLoop = audioDuration > 0.1;
2933
+ if (!safeToLoop) {
2934
+ console.warn(`[StarAudio] \u26A0\uFE0F Audio "${id}" is too short (${audioDuration.toFixed(3)}s) to loop safely - disabling loop to prevent crash`);
2935
+ } else {
2936
+ howl2.loop(true);
2937
+ }
2938
+ }
2939
+ this._sounds.set(id, howl2);
2940
+ },
2941
+ onloaderror: (_id, error) => {
2942
+ console.warn(`[StarAudio] \u26A0\uFE0F Failed to auto-load "${id}" - sound will not play.`, error);
2943
+ this._eventTarget.dispatchEvent(new Event("error"));
2944
+ this._soundGroups.delete(id);
2945
+ }
2946
+ });
2947
+ return null;
2948
+ }
2949
+ const howl = this._sounds.get(id);
2950
+ if (!howl) {
2951
+ console.warn(`[StarAudio] Sound not loaded: ${id}. Available presets: beep, coin, pickup, jump, hurt, shoot, laser, explosion, powerup, click, success, bonus, error`);
2952
+ return null;
2953
+ }
2954
+ const group = this._soundGroups.get(id) || "sfx";
2955
+ const perSoundVolume = this._soundVolumes.get(id);
2956
+ const effectiveVolume = opts.volume ?? perSoundVolume ?? this._volumes[group];
2957
+ if (effectiveVolume !== void 0) {
2958
+ howl.volume(effectiveVolume);
2959
+ }
2960
+ if (opts.loop !== void 0) {
2961
+ const audioDuration = howl.duration();
2962
+ const safeToLoop = opts.loop && audioDuration > 0.1;
2963
+ if (opts.loop && !safeToLoop) {
2964
+ console.warn(`[StarAudio] \u26A0\uFE0F Audio "${id}" is too short (${audioDuration.toFixed(3)}s) to loop safely - disabling loop to prevent crash`);
2965
+ }
2966
+ howl.loop(safeToLoop);
2967
+ }
2968
+ if (opts.rate !== void 0) {
2969
+ howl.rate(opts.rate);
2970
+ }
2971
+ try {
2972
+ const soundId = howl.play();
2973
+ const group2 = this._soundGroups.get(id) || "sfx";
2974
+ if (group2 === "sfx") {
2975
+ }
2976
+ return new SoundHandleImpl(id, howl, soundId);
2977
+ } catch (error) {
2978
+ console.warn(`[StarAudio] Failed to play ${id}:`, error);
2979
+ this._eventTarget.dispatchEvent(new Event("error"));
2980
+ return null;
2981
+ }
2982
+ }
2983
+ // --- Volume & Mute ---
2984
+ setMusicVolume(v, _o = {}) {
2985
+ const clampedVolume = Math.max(0, Math.min(1, v));
2986
+ this._volumes.music = clampedVolume;
2987
+ for (const [id, howl] of this._sounds.entries()) {
2988
+ if (this._soundGroups.get(id) === "music") {
2989
+ howl.volume(clampedVolume);
2990
+ }
2991
+ }
2992
+ this._saveState();
2993
+ }
2994
+ setSfxVolume(v, _o = {}) {
2995
+ const clampedVolume = Math.max(0, Math.min(1, v));
2996
+ this._volumes.sfx = clampedVolume;
2997
+ for (const [id, howl] of this._sounds.entries()) {
2998
+ if (this._soundGroups.get(id) === "sfx") {
2999
+ howl.volume(clampedVolume);
3000
+ }
3001
+ }
3002
+ this._saveState();
3003
+ }
3004
+ setMute(muted) {
3005
+ this._isMuted = muted;
3006
+ import_howler.Howler.volume(muted ? 0 : 1);
3007
+ this._saveState();
3008
+ }
3009
+ toggleMute() {
3010
+ this.setMute(!this._isMuted);
3011
+ }
3012
+ isMuted() {
3013
+ return this._isMuted;
3014
+ }
3015
+ attachUnlock(target) {
3016
+ if (this._unlockHandlerRemover) {
3017
+ this._unlockHandlerRemover();
3018
+ this._unlockHandlerRemover = null;
3019
+ }
3020
+ let effectiveTarget = target || window;
3021
+ if (effectiveTarget === window && window.self !== window.top) {
3022
+ console.log("[StarAudio] Detected iframe context, attaching unlock to document");
3023
+ effectiveTarget = document;
3024
+ }
3025
+ if (!effectiveTarget) return;
3026
+ const handler = () => {
3027
+ console.log("[StarAudio] User interaction detected");
3028
+ this._unlock();
3029
+ };
3030
+ const events = ["pointerdown", "touchstart", "keydown", "click"];
3031
+ events.forEach((eventType) => {
3032
+ effectiveTarget.addEventListener(eventType, handler, { once: true, capture: true });
3033
+ });
3034
+ this._unlockHandlerRemover = () => {
3035
+ events.forEach((eventType) => {
3036
+ effectiveTarget.removeEventListener(eventType, handler, { capture: true });
3037
+ });
3038
+ };
3039
+ }
3040
+ // --- Events ---
3041
+ on(evt, cb) {
3042
+ this._eventTarget.addEventListener(evt, cb);
3043
+ }
3044
+ off(evt, cb) {
3045
+ this._eventTarget.removeEventListener(evt, cb);
3046
+ }
3047
+ // --- Cleanup ---
3048
+ destroy() {
3049
+ if (this._unlockHandlerRemover) {
3050
+ this._unlockHandlerRemover();
3051
+ }
3052
+ if (this._visibilityHandler) {
3053
+ document.removeEventListener("visibilitychange", this._visibilityHandler);
3054
+ }
3055
+ if (this._pendingFadeTimeout !== null) {
3056
+ clearTimeout(this._pendingFadeTimeout);
3057
+ this._pendingFadeTimeout = null;
3058
+ }
3059
+ for (const howl of this._sounds.values()) {
3060
+ howl.unload();
3061
+ }
3062
+ this._sounds.clear();
3063
+ for (const url of this._generatedWavUrls.values()) {
3064
+ URL.revokeObjectURL(url);
3065
+ }
3066
+ this._generatedWavUrls.clear();
3067
+ }
3068
+ // --- Private Methods ---
3069
+ _unlock() {
3070
+ if (this._state === "running") return;
3071
+ this._state = "running";
3072
+ this._eventTarget.dispatchEvent(new Event("unlocked"));
3073
+ console.log("[StarAudio] \u2705 Unlocked (Howler handling audio context)");
3074
+ if (this._readyResolver) {
3075
+ this._readyResolver();
3076
+ this._readyResolver = null;
3077
+ }
3078
+ }
3079
+ _inferGroup(id) {
3080
+ const lower = id.toLowerCase();
3081
+ if (lower.startsWith("music.") || lower.startsWith("bgm.")) {
3082
+ return "music";
3083
+ }
3084
+ return "sfx";
3085
+ }
3086
+ _loadState() {
3087
+ try {
3088
+ const stored = localStorage.getItem(this._persistKey);
3089
+ if (stored) {
3090
+ const state = JSON.parse(stored);
3091
+ this._isMuted = state.mute;
3092
+ this._volumes = state.volumes;
3093
+ }
3094
+ } catch (e) {
3095
+ console.warn("[StarAudio] Could not load persisted state.");
3096
+ }
3097
+ }
3098
+ _saveState() {
3099
+ try {
3100
+ const state = {
3101
+ mute: this._isMuted,
3102
+ volumes: this._volumes
3103
+ };
3104
+ localStorage.setItem(this._persistKey, JSON.stringify(state));
3105
+ } catch (e) {
3106
+ console.warn("[StarAudio] Could not save persisted state.");
3107
+ }
3108
+ }
3109
+ };
3110
+
3111
+ // src/index.ts
3112
+ var VERSION = "0.1.0";
3113
+ function createStarAudio(opts) {
3114
+ if (typeof window === "undefined" || !(window.AudioContext || window.webkitAudioContext)) {
3115
+ throw new Error("[StarAudio] createStarAudio must be called in a browser (no AudioContext).");
3116
+ }
3117
+ return new StarAudioImpl(opts);
3118
+ }
3119
+ var index_default = createStarAudio;
3120
+ // Annotate the CommonJS export names for ESM import in node:
3121
+ 0 && (module.exports = {
3122
+ VERSION,
3123
+ createStarAudio
3124
+ });
3125
+ /*! Bundled license information:
3126
+
3127
+ howler/dist/howler.js:
3128
+ (*!
3129
+ * howler.js v2.2.4
3130
+ * howlerjs.com
3131
+ *
3132
+ * (c) 2013-2020, James Simpson of GoldFire Studios
3133
+ * goldfirestudios.com
3134
+ *
3135
+ * MIT License
3136
+ *)
3137
+ (*!
3138
+ * Spatial Plugin - Adds support for stereo and 3D audio where Web Audio is supported.
3139
+ *
3140
+ * howler.js v2.2.4
3141
+ * howlerjs.com
3142
+ *
3143
+ * (c) 2013-2020, James Simpson of GoldFire Studios
3144
+ * goldfirestudios.com
3145
+ *
3146
+ * MIT License
3147
+ *)
3148
+ */
3149
+ //# sourceMappingURL=index.js.map