@mingxy/ocosay 1.0.20 → 1.0.22
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/README.md +17 -3
- package/dist/core/backends/howler-backend.d.ts +31 -0
- package/dist/core/backends/howler-backend.d.ts.map +1 -0
- package/dist/core/backends/howler-backend.js +183 -0
- package/dist/core/backends/howler-backend.js.map +1 -0
- package/dist/core/backends/index.d.ts +2 -0
- package/dist/core/backends/index.d.ts.map +1 -1
- package/dist/core/backends/index.js +5 -0
- package/dist/core/backends/index.js.map +1 -1
- package/dist/core/player.d.ts +2 -12
- package/dist/core/player.d.ts.map +1 -1
- package/dist/core/player.js +44 -85
- package/dist/core/player.js.map +1 -1
- package/dist/core/stream-player.d.ts +10 -23
- package/dist/core/stream-player.d.ts.map +1 -1
- package/dist/core/stream-player.js +66 -154
- package/dist/core/stream-player.js.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +3210 -304
- package/dist/plugin.js.map +4 -4
- package/dist/tools/tts.d.ts +0 -4
- package/dist/tools/tts.d.ts.map +1 -1
- package/dist/tools/tts.js +28 -16
- package/dist/tools/tts.js.map +1 -1
- package/package.json +3 -1
package/dist/plugin.js
CHANGED
|
@@ -1,128 +1,3111 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
8
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
9
|
+
}) : x)(function(x) {
|
|
10
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
11
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
12
|
+
});
|
|
13
|
+
var __commonJS = (cb, mod) => function __require2() {
|
|
14
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
15
|
+
};
|
|
16
|
+
var __copyProps = (to, from, except, desc) => {
|
|
17
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
18
|
+
for (let key of __getOwnPropNames(from))
|
|
19
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
20
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
21
|
+
}
|
|
22
|
+
return to;
|
|
23
|
+
};
|
|
24
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
25
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
26
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
27
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
28
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
29
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
30
|
+
mod
|
|
31
|
+
));
|
|
32
|
+
|
|
33
|
+
// node_modules/howler/dist/howler.js
|
|
34
|
+
var require_howler = __commonJS({
|
|
35
|
+
"node_modules/howler/dist/howler.js"(exports) {
|
|
36
|
+
(function() {
|
|
37
|
+
"use strict";
|
|
38
|
+
var HowlerGlobal2 = function() {
|
|
39
|
+
this.init();
|
|
40
|
+
};
|
|
41
|
+
HowlerGlobal2.prototype = {
|
|
42
|
+
/**
|
|
43
|
+
* Initialize the global Howler object.
|
|
44
|
+
* @return {Howler}
|
|
45
|
+
*/
|
|
46
|
+
init: function() {
|
|
47
|
+
var self = this || Howler2;
|
|
48
|
+
self._counter = 1e3;
|
|
49
|
+
self._html5AudioPool = [];
|
|
50
|
+
self.html5PoolSize = 10;
|
|
51
|
+
self._codecs = {};
|
|
52
|
+
self._howls = [];
|
|
53
|
+
self._muted = false;
|
|
54
|
+
self._volume = 1;
|
|
55
|
+
self._canPlayEvent = "canplaythrough";
|
|
56
|
+
self._navigator = typeof window !== "undefined" && window.navigator ? window.navigator : null;
|
|
57
|
+
self.masterGain = null;
|
|
58
|
+
self.noAudio = false;
|
|
59
|
+
self.usingWebAudio = true;
|
|
60
|
+
self.autoSuspend = true;
|
|
61
|
+
self.ctx = null;
|
|
62
|
+
self.autoUnlock = true;
|
|
63
|
+
self._setup();
|
|
64
|
+
return self;
|
|
65
|
+
},
|
|
66
|
+
/**
|
|
67
|
+
* Get/set the global volume for all sounds.
|
|
68
|
+
* @param {Float} vol Volume from 0.0 to 1.0.
|
|
69
|
+
* @return {Howler/Float} Returns self or current volume.
|
|
70
|
+
*/
|
|
71
|
+
volume: function(vol) {
|
|
72
|
+
var self = this || Howler2;
|
|
73
|
+
vol = parseFloat(vol);
|
|
74
|
+
if (!self.ctx) {
|
|
75
|
+
setupAudioContext();
|
|
76
|
+
}
|
|
77
|
+
if (typeof vol !== "undefined" && vol >= 0 && vol <= 1) {
|
|
78
|
+
self._volume = vol;
|
|
79
|
+
if (self._muted) {
|
|
80
|
+
return self;
|
|
81
|
+
}
|
|
82
|
+
if (self.usingWebAudio) {
|
|
83
|
+
self.masterGain.gain.setValueAtTime(vol, Howler2.ctx.currentTime);
|
|
84
|
+
}
|
|
85
|
+
for (var i = 0; i < self._howls.length; i++) {
|
|
86
|
+
if (!self._howls[i]._webAudio) {
|
|
87
|
+
var ids = self._howls[i]._getSoundIds();
|
|
88
|
+
for (var j = 0; j < ids.length; j++) {
|
|
89
|
+
var sound = self._howls[i]._soundById(ids[j]);
|
|
90
|
+
if (sound && sound._node) {
|
|
91
|
+
sound._node.volume = sound._volume * vol;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return self;
|
|
97
|
+
}
|
|
98
|
+
return self._volume;
|
|
99
|
+
},
|
|
100
|
+
/**
|
|
101
|
+
* Handle muting and unmuting globally.
|
|
102
|
+
* @param {Boolean} muted Is muted or not.
|
|
103
|
+
*/
|
|
104
|
+
mute: function(muted) {
|
|
105
|
+
var self = this || Howler2;
|
|
106
|
+
if (!self.ctx) {
|
|
107
|
+
setupAudioContext();
|
|
108
|
+
}
|
|
109
|
+
self._muted = muted;
|
|
110
|
+
if (self.usingWebAudio) {
|
|
111
|
+
self.masterGain.gain.setValueAtTime(muted ? 0 : self._volume, Howler2.ctx.currentTime);
|
|
112
|
+
}
|
|
113
|
+
for (var i = 0; i < self._howls.length; i++) {
|
|
114
|
+
if (!self._howls[i]._webAudio) {
|
|
115
|
+
var ids = self._howls[i]._getSoundIds();
|
|
116
|
+
for (var j = 0; j < ids.length; j++) {
|
|
117
|
+
var sound = self._howls[i]._soundById(ids[j]);
|
|
118
|
+
if (sound && sound._node) {
|
|
119
|
+
sound._node.muted = muted ? true : sound._muted;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return self;
|
|
125
|
+
},
|
|
126
|
+
/**
|
|
127
|
+
* Handle stopping all sounds globally.
|
|
128
|
+
*/
|
|
129
|
+
stop: function() {
|
|
130
|
+
var self = this || Howler2;
|
|
131
|
+
for (var i = 0; i < self._howls.length; i++) {
|
|
132
|
+
self._howls[i].stop();
|
|
133
|
+
}
|
|
134
|
+
return self;
|
|
135
|
+
},
|
|
136
|
+
/**
|
|
137
|
+
* Unload and destroy all currently loaded Howl objects.
|
|
138
|
+
* @return {Howler}
|
|
139
|
+
*/
|
|
140
|
+
unload: function() {
|
|
141
|
+
var self = this || Howler2;
|
|
142
|
+
for (var i = self._howls.length - 1; i >= 0; i--) {
|
|
143
|
+
self._howls[i].unload();
|
|
144
|
+
}
|
|
145
|
+
if (self.usingWebAudio && self.ctx && typeof self.ctx.close !== "undefined") {
|
|
146
|
+
self.ctx.close();
|
|
147
|
+
self.ctx = null;
|
|
148
|
+
setupAudioContext();
|
|
149
|
+
}
|
|
150
|
+
return self;
|
|
151
|
+
},
|
|
152
|
+
/**
|
|
153
|
+
* Check for codec support of specific extension.
|
|
154
|
+
* @param {String} ext Audio file extention.
|
|
155
|
+
* @return {Boolean}
|
|
156
|
+
*/
|
|
157
|
+
codecs: function(ext) {
|
|
158
|
+
return (this || Howler2)._codecs[ext.replace(/^x-/, "")];
|
|
159
|
+
},
|
|
160
|
+
/**
|
|
161
|
+
* Setup various state values for global tracking.
|
|
162
|
+
* @return {Howler}
|
|
163
|
+
*/
|
|
164
|
+
_setup: function() {
|
|
165
|
+
var self = this || Howler2;
|
|
166
|
+
self.state = self.ctx ? self.ctx.state || "suspended" : "suspended";
|
|
167
|
+
self._autoSuspend();
|
|
168
|
+
if (!self.usingWebAudio) {
|
|
169
|
+
if (typeof Audio !== "undefined") {
|
|
170
|
+
try {
|
|
171
|
+
var test = new Audio();
|
|
172
|
+
if (typeof test.oncanplaythrough === "undefined") {
|
|
173
|
+
self._canPlayEvent = "canplay";
|
|
174
|
+
}
|
|
175
|
+
} catch (e) {
|
|
176
|
+
self.noAudio = true;
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
self.noAudio = true;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
var test = new Audio();
|
|
184
|
+
if (test.muted) {
|
|
185
|
+
self.noAudio = true;
|
|
186
|
+
}
|
|
187
|
+
} catch (e) {
|
|
188
|
+
}
|
|
189
|
+
if (!self.noAudio) {
|
|
190
|
+
self._setupCodecs();
|
|
191
|
+
}
|
|
192
|
+
return self;
|
|
193
|
+
},
|
|
194
|
+
/**
|
|
195
|
+
* Check for browser support for various codecs and cache the results.
|
|
196
|
+
* @return {Howler}
|
|
197
|
+
*/
|
|
198
|
+
_setupCodecs: function() {
|
|
199
|
+
var self = this || Howler2;
|
|
200
|
+
var audioTest = null;
|
|
201
|
+
try {
|
|
202
|
+
audioTest = typeof Audio !== "undefined" ? new Audio() : null;
|
|
203
|
+
} catch (err) {
|
|
204
|
+
return self;
|
|
205
|
+
}
|
|
206
|
+
if (!audioTest || typeof audioTest.canPlayType !== "function") {
|
|
207
|
+
return self;
|
|
208
|
+
}
|
|
209
|
+
var mpegTest = audioTest.canPlayType("audio/mpeg;").replace(/^no$/, "");
|
|
210
|
+
var ua = self._navigator ? self._navigator.userAgent : "";
|
|
211
|
+
var checkOpera = ua.match(/OPR\/(\d+)/g);
|
|
212
|
+
var isOldOpera = checkOpera && parseInt(checkOpera[0].split("/")[1], 10) < 33;
|
|
213
|
+
var checkSafari = ua.indexOf("Safari") !== -1 && ua.indexOf("Chrome") === -1;
|
|
214
|
+
var safariVersion = ua.match(/Version\/(.*?) /);
|
|
215
|
+
var isOldSafari = checkSafari && safariVersion && parseInt(safariVersion[1], 10) < 15;
|
|
216
|
+
self._codecs = {
|
|
217
|
+
mp3: !!(!isOldOpera && (mpegTest || audioTest.canPlayType("audio/mp3;").replace(/^no$/, ""))),
|
|
218
|
+
mpeg: !!mpegTest,
|
|
219
|
+
opus: !!audioTest.canPlayType('audio/ogg; codecs="opus"').replace(/^no$/, ""),
|
|
220
|
+
ogg: !!audioTest.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/, ""),
|
|
221
|
+
oga: !!audioTest.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/, ""),
|
|
222
|
+
wav: !!(audioTest.canPlayType('audio/wav; codecs="1"') || audioTest.canPlayType("audio/wav")).replace(/^no$/, ""),
|
|
223
|
+
aac: !!audioTest.canPlayType("audio/aac;").replace(/^no$/, ""),
|
|
224
|
+
caf: !!audioTest.canPlayType("audio/x-caf;").replace(/^no$/, ""),
|
|
225
|
+
m4a: !!(audioTest.canPlayType("audio/x-m4a;") || audioTest.canPlayType("audio/m4a;") || audioTest.canPlayType("audio/aac;")).replace(/^no$/, ""),
|
|
226
|
+
m4b: !!(audioTest.canPlayType("audio/x-m4b;") || audioTest.canPlayType("audio/m4b;") || audioTest.canPlayType("audio/aac;")).replace(/^no$/, ""),
|
|
227
|
+
mp4: !!(audioTest.canPlayType("audio/x-mp4;") || audioTest.canPlayType("audio/mp4;") || audioTest.canPlayType("audio/aac;")).replace(/^no$/, ""),
|
|
228
|
+
weba: !!(!isOldSafari && audioTest.canPlayType('audio/webm; codecs="vorbis"').replace(/^no$/, "")),
|
|
229
|
+
webm: !!(!isOldSafari && audioTest.canPlayType('audio/webm; codecs="vorbis"').replace(/^no$/, "")),
|
|
230
|
+
dolby: !!audioTest.canPlayType('audio/mp4; codecs="ec-3"').replace(/^no$/, ""),
|
|
231
|
+
flac: !!(audioTest.canPlayType("audio/x-flac;") || audioTest.canPlayType("audio/flac;")).replace(/^no$/, "")
|
|
232
|
+
};
|
|
233
|
+
return self;
|
|
234
|
+
},
|
|
235
|
+
/**
|
|
236
|
+
* Some browsers/devices will only allow audio to be played after a user interaction.
|
|
237
|
+
* Attempt to automatically unlock audio on the first user interaction.
|
|
238
|
+
* Concept from: http://paulbakaus.com/tutorials/html5/web-audio-on-ios/
|
|
239
|
+
* @return {Howler}
|
|
240
|
+
*/
|
|
241
|
+
_unlockAudio: function() {
|
|
242
|
+
var self = this || Howler2;
|
|
243
|
+
if (self._audioUnlocked || !self.ctx) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
self._audioUnlocked = false;
|
|
247
|
+
self.autoUnlock = false;
|
|
248
|
+
if (!self._mobileUnloaded && self.ctx.sampleRate !== 44100) {
|
|
249
|
+
self._mobileUnloaded = true;
|
|
250
|
+
self.unload();
|
|
251
|
+
}
|
|
252
|
+
self._scratchBuffer = self.ctx.createBuffer(1, 1, 22050);
|
|
253
|
+
var unlock = function(e) {
|
|
254
|
+
while (self._html5AudioPool.length < self.html5PoolSize) {
|
|
255
|
+
try {
|
|
256
|
+
var audioNode = new Audio();
|
|
257
|
+
audioNode._unlocked = true;
|
|
258
|
+
self._releaseHtml5Audio(audioNode);
|
|
259
|
+
} catch (e2) {
|
|
260
|
+
self.noAudio = true;
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
for (var i = 0; i < self._howls.length; i++) {
|
|
265
|
+
if (!self._howls[i]._webAudio) {
|
|
266
|
+
var ids = self._howls[i]._getSoundIds();
|
|
267
|
+
for (var j = 0; j < ids.length; j++) {
|
|
268
|
+
var sound = self._howls[i]._soundById(ids[j]);
|
|
269
|
+
if (sound && sound._node && !sound._node._unlocked) {
|
|
270
|
+
sound._node._unlocked = true;
|
|
271
|
+
sound._node.load();
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
self._autoResume();
|
|
277
|
+
var source = self.ctx.createBufferSource();
|
|
278
|
+
source.buffer = self._scratchBuffer;
|
|
279
|
+
source.connect(self.ctx.destination);
|
|
280
|
+
if (typeof source.start === "undefined") {
|
|
281
|
+
source.noteOn(0);
|
|
282
|
+
} else {
|
|
283
|
+
source.start(0);
|
|
284
|
+
}
|
|
285
|
+
if (typeof self.ctx.resume === "function") {
|
|
286
|
+
self.ctx.resume();
|
|
287
|
+
}
|
|
288
|
+
source.onended = function() {
|
|
289
|
+
source.disconnect(0);
|
|
290
|
+
self._audioUnlocked = true;
|
|
291
|
+
document.removeEventListener("touchstart", unlock, true);
|
|
292
|
+
document.removeEventListener("touchend", unlock, true);
|
|
293
|
+
document.removeEventListener("click", unlock, true);
|
|
294
|
+
document.removeEventListener("keydown", unlock, true);
|
|
295
|
+
for (var i2 = 0; i2 < self._howls.length; i2++) {
|
|
296
|
+
self._howls[i2]._emit("unlock");
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
};
|
|
300
|
+
document.addEventListener("touchstart", unlock, true);
|
|
301
|
+
document.addEventListener("touchend", unlock, true);
|
|
302
|
+
document.addEventListener("click", unlock, true);
|
|
303
|
+
document.addEventListener("keydown", unlock, true);
|
|
304
|
+
return self;
|
|
305
|
+
},
|
|
306
|
+
/**
|
|
307
|
+
* Get an unlocked HTML5 Audio object from the pool. If none are left,
|
|
308
|
+
* return a new Audio object and throw a warning.
|
|
309
|
+
* @return {Audio} HTML5 Audio object.
|
|
310
|
+
*/
|
|
311
|
+
_obtainHtml5Audio: function() {
|
|
312
|
+
var self = this || Howler2;
|
|
313
|
+
if (self._html5AudioPool.length) {
|
|
314
|
+
return self._html5AudioPool.pop();
|
|
315
|
+
}
|
|
316
|
+
var testPlay = new Audio().play();
|
|
317
|
+
if (testPlay && typeof Promise !== "undefined" && (testPlay instanceof Promise || typeof testPlay.then === "function")) {
|
|
318
|
+
testPlay.catch(function() {
|
|
319
|
+
console.warn("HTML5 Audio pool exhausted, returning potentially locked audio object.");
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
return new Audio();
|
|
323
|
+
},
|
|
324
|
+
/**
|
|
325
|
+
* Return an activated HTML5 Audio object to the pool.
|
|
326
|
+
* @return {Howler}
|
|
327
|
+
*/
|
|
328
|
+
_releaseHtml5Audio: function(audio) {
|
|
329
|
+
var self = this || Howler2;
|
|
330
|
+
if (audio._unlocked) {
|
|
331
|
+
self._html5AudioPool.push(audio);
|
|
332
|
+
}
|
|
333
|
+
return self;
|
|
334
|
+
},
|
|
335
|
+
/**
|
|
336
|
+
* Automatically suspend the Web Audio AudioContext after no sound has played for 30 seconds.
|
|
337
|
+
* This saves processing/energy and fixes various browser-specific bugs with audio getting stuck.
|
|
338
|
+
* @return {Howler}
|
|
339
|
+
*/
|
|
340
|
+
_autoSuspend: function() {
|
|
341
|
+
var self = this;
|
|
342
|
+
if (!self.autoSuspend || !self.ctx || typeof self.ctx.suspend === "undefined" || !Howler2.usingWebAudio) {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
for (var i = 0; i < self._howls.length; i++) {
|
|
346
|
+
if (self._howls[i]._webAudio) {
|
|
347
|
+
for (var j = 0; j < self._howls[i]._sounds.length; j++) {
|
|
348
|
+
if (!self._howls[i]._sounds[j]._paused) {
|
|
349
|
+
return self;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
if (self._suspendTimer) {
|
|
355
|
+
clearTimeout(self._suspendTimer);
|
|
356
|
+
}
|
|
357
|
+
self._suspendTimer = setTimeout(function() {
|
|
358
|
+
if (!self.autoSuspend) {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
self._suspendTimer = null;
|
|
362
|
+
self.state = "suspending";
|
|
363
|
+
var handleSuspension = function() {
|
|
364
|
+
self.state = "suspended";
|
|
365
|
+
if (self._resumeAfterSuspend) {
|
|
366
|
+
delete self._resumeAfterSuspend;
|
|
367
|
+
self._autoResume();
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
self.ctx.suspend().then(handleSuspension, handleSuspension);
|
|
371
|
+
}, 3e4);
|
|
372
|
+
return self;
|
|
373
|
+
},
|
|
374
|
+
/**
|
|
375
|
+
* Automatically resume the Web Audio AudioContext when a new sound is played.
|
|
376
|
+
* @return {Howler}
|
|
377
|
+
*/
|
|
378
|
+
_autoResume: function() {
|
|
379
|
+
var self = this;
|
|
380
|
+
if (!self.ctx || typeof self.ctx.resume === "undefined" || !Howler2.usingWebAudio) {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
if (self.state === "running" && self.ctx.state !== "interrupted" && self._suspendTimer) {
|
|
384
|
+
clearTimeout(self._suspendTimer);
|
|
385
|
+
self._suspendTimer = null;
|
|
386
|
+
} else if (self.state === "suspended" || self.state === "running" && self.ctx.state === "interrupted") {
|
|
387
|
+
self.ctx.resume().then(function() {
|
|
388
|
+
self.state = "running";
|
|
389
|
+
for (var i = 0; i < self._howls.length; i++) {
|
|
390
|
+
self._howls[i]._emit("resume");
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
if (self._suspendTimer) {
|
|
394
|
+
clearTimeout(self._suspendTimer);
|
|
395
|
+
self._suspendTimer = null;
|
|
396
|
+
}
|
|
397
|
+
} else if (self.state === "suspending") {
|
|
398
|
+
self._resumeAfterSuspend = true;
|
|
399
|
+
}
|
|
400
|
+
return self;
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
var Howler2 = new HowlerGlobal2();
|
|
404
|
+
var Howl3 = function(o) {
|
|
405
|
+
var self = this;
|
|
406
|
+
if (!o.src || o.src.length === 0) {
|
|
407
|
+
console.error("An array of source files must be passed with any new Howl.");
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
self.init(o);
|
|
411
|
+
};
|
|
412
|
+
Howl3.prototype = {
|
|
413
|
+
/**
|
|
414
|
+
* Initialize a new Howl group object.
|
|
415
|
+
* @param {Object} o Passed in properties for this group.
|
|
416
|
+
* @return {Howl}
|
|
417
|
+
*/
|
|
418
|
+
init: function(o) {
|
|
419
|
+
var self = this;
|
|
420
|
+
if (!Howler2.ctx) {
|
|
421
|
+
setupAudioContext();
|
|
422
|
+
}
|
|
423
|
+
self._autoplay = o.autoplay || false;
|
|
424
|
+
self._format = typeof o.format !== "string" ? o.format : [o.format];
|
|
425
|
+
self._html5 = o.html5 || false;
|
|
426
|
+
self._muted = o.mute || false;
|
|
427
|
+
self._loop = o.loop || false;
|
|
428
|
+
self._pool = o.pool || 5;
|
|
429
|
+
self._preload = typeof o.preload === "boolean" || o.preload === "metadata" ? o.preload : true;
|
|
430
|
+
self._rate = o.rate || 1;
|
|
431
|
+
self._sprite = o.sprite || {};
|
|
432
|
+
self._src = typeof o.src !== "string" ? o.src : [o.src];
|
|
433
|
+
self._volume = o.volume !== void 0 ? o.volume : 1;
|
|
434
|
+
self._xhr = {
|
|
435
|
+
method: o.xhr && o.xhr.method ? o.xhr.method : "GET",
|
|
436
|
+
headers: o.xhr && o.xhr.headers ? o.xhr.headers : null,
|
|
437
|
+
withCredentials: o.xhr && o.xhr.withCredentials ? o.xhr.withCredentials : false
|
|
438
|
+
};
|
|
439
|
+
self._duration = 0;
|
|
440
|
+
self._state = "unloaded";
|
|
441
|
+
self._sounds = [];
|
|
442
|
+
self._endTimers = {};
|
|
443
|
+
self._queue = [];
|
|
444
|
+
self._playLock = false;
|
|
445
|
+
self._onend = o.onend ? [{ fn: o.onend }] : [];
|
|
446
|
+
self._onfade = o.onfade ? [{ fn: o.onfade }] : [];
|
|
447
|
+
self._onload = o.onload ? [{ fn: o.onload }] : [];
|
|
448
|
+
self._onloaderror = o.onloaderror ? [{ fn: o.onloaderror }] : [];
|
|
449
|
+
self._onplayerror = o.onplayerror ? [{ fn: o.onplayerror }] : [];
|
|
450
|
+
self._onpause = o.onpause ? [{ fn: o.onpause }] : [];
|
|
451
|
+
self._onplay = o.onplay ? [{ fn: o.onplay }] : [];
|
|
452
|
+
self._onstop = o.onstop ? [{ fn: o.onstop }] : [];
|
|
453
|
+
self._onmute = o.onmute ? [{ fn: o.onmute }] : [];
|
|
454
|
+
self._onvolume = o.onvolume ? [{ fn: o.onvolume }] : [];
|
|
455
|
+
self._onrate = o.onrate ? [{ fn: o.onrate }] : [];
|
|
456
|
+
self._onseek = o.onseek ? [{ fn: o.onseek }] : [];
|
|
457
|
+
self._onunlock = o.onunlock ? [{ fn: o.onunlock }] : [];
|
|
458
|
+
self._onresume = [];
|
|
459
|
+
self._webAudio = Howler2.usingWebAudio && !self._html5;
|
|
460
|
+
if (typeof Howler2.ctx !== "undefined" && Howler2.ctx && Howler2.autoUnlock) {
|
|
461
|
+
Howler2._unlockAudio();
|
|
462
|
+
}
|
|
463
|
+
Howler2._howls.push(self);
|
|
464
|
+
if (self._autoplay) {
|
|
465
|
+
self._queue.push({
|
|
466
|
+
event: "play",
|
|
467
|
+
action: function() {
|
|
468
|
+
self.play();
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
if (self._preload && self._preload !== "none") {
|
|
473
|
+
self.load();
|
|
474
|
+
}
|
|
475
|
+
return self;
|
|
476
|
+
},
|
|
477
|
+
/**
|
|
478
|
+
* Load the audio file.
|
|
479
|
+
* @return {Howler}
|
|
480
|
+
*/
|
|
481
|
+
load: function() {
|
|
482
|
+
var self = this;
|
|
483
|
+
var url = null;
|
|
484
|
+
if (Howler2.noAudio) {
|
|
485
|
+
self._emit("loaderror", null, "No audio support.");
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
if (typeof self._src === "string") {
|
|
489
|
+
self._src = [self._src];
|
|
490
|
+
}
|
|
491
|
+
for (var i = 0; i < self._src.length; i++) {
|
|
492
|
+
var ext, str;
|
|
493
|
+
if (self._format && self._format[i]) {
|
|
494
|
+
ext = self._format[i];
|
|
495
|
+
} else {
|
|
496
|
+
str = self._src[i];
|
|
497
|
+
if (typeof str !== "string") {
|
|
498
|
+
self._emit("loaderror", null, "Non-string found in selected audio sources - ignoring.");
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
ext = /^data:audio\/([^;,]+);/i.exec(str);
|
|
502
|
+
if (!ext) {
|
|
503
|
+
ext = /\.([^.]+)$/.exec(str.split("?", 1)[0]);
|
|
504
|
+
}
|
|
505
|
+
if (ext) {
|
|
506
|
+
ext = ext[1].toLowerCase();
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
if (!ext) {
|
|
510
|
+
console.warn('No file extension was found. Consider using the "format" property or specify an extension.');
|
|
511
|
+
}
|
|
512
|
+
if (ext && Howler2.codecs(ext)) {
|
|
513
|
+
url = self._src[i];
|
|
514
|
+
break;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
if (!url) {
|
|
518
|
+
self._emit("loaderror", null, "No codec support for selected audio sources.");
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
self._src = url;
|
|
522
|
+
self._state = "loading";
|
|
523
|
+
if (window.location.protocol === "https:" && url.slice(0, 5) === "http:") {
|
|
524
|
+
self._html5 = true;
|
|
525
|
+
self._webAudio = false;
|
|
526
|
+
}
|
|
527
|
+
new Sound2(self);
|
|
528
|
+
if (self._webAudio) {
|
|
529
|
+
loadBuffer(self);
|
|
530
|
+
}
|
|
531
|
+
return self;
|
|
532
|
+
},
|
|
533
|
+
/**
|
|
534
|
+
* Play a sound or resume previous playback.
|
|
535
|
+
* @param {String/Number} sprite Sprite name for sprite playback or sound id to continue previous.
|
|
536
|
+
* @param {Boolean} internal Internal Use: true prevents event firing.
|
|
537
|
+
* @return {Number} Sound ID.
|
|
538
|
+
*/
|
|
539
|
+
play: function(sprite, internal) {
|
|
540
|
+
var self = this;
|
|
541
|
+
var id = null;
|
|
542
|
+
if (typeof sprite === "number") {
|
|
543
|
+
id = sprite;
|
|
544
|
+
sprite = null;
|
|
545
|
+
} else if (typeof sprite === "string" && self._state === "loaded" && !self._sprite[sprite]) {
|
|
546
|
+
return null;
|
|
547
|
+
} else if (typeof sprite === "undefined") {
|
|
548
|
+
sprite = "__default";
|
|
549
|
+
if (!self._playLock) {
|
|
550
|
+
var num = 0;
|
|
551
|
+
for (var i = 0; i < self._sounds.length; i++) {
|
|
552
|
+
if (self._sounds[i]._paused && !self._sounds[i]._ended) {
|
|
553
|
+
num++;
|
|
554
|
+
id = self._sounds[i]._id;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
if (num === 1) {
|
|
558
|
+
sprite = null;
|
|
559
|
+
} else {
|
|
560
|
+
id = null;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
var sound = id ? self._soundById(id) : self._inactiveSound();
|
|
565
|
+
if (!sound) {
|
|
566
|
+
return null;
|
|
567
|
+
}
|
|
568
|
+
if (id && !sprite) {
|
|
569
|
+
sprite = sound._sprite || "__default";
|
|
570
|
+
}
|
|
571
|
+
if (self._state !== "loaded") {
|
|
572
|
+
sound._sprite = sprite;
|
|
573
|
+
sound._ended = false;
|
|
574
|
+
var soundId = sound._id;
|
|
575
|
+
self._queue.push({
|
|
576
|
+
event: "play",
|
|
577
|
+
action: function() {
|
|
578
|
+
self.play(soundId);
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
return soundId;
|
|
582
|
+
}
|
|
583
|
+
if (id && !sound._paused) {
|
|
584
|
+
if (!internal) {
|
|
585
|
+
self._loadQueue("play");
|
|
586
|
+
}
|
|
587
|
+
return sound._id;
|
|
588
|
+
}
|
|
589
|
+
if (self._webAudio) {
|
|
590
|
+
Howler2._autoResume();
|
|
591
|
+
}
|
|
592
|
+
var seek = Math.max(0, sound._seek > 0 ? sound._seek : self._sprite[sprite][0] / 1e3);
|
|
593
|
+
var duration = Math.max(0, (self._sprite[sprite][0] + self._sprite[sprite][1]) / 1e3 - seek);
|
|
594
|
+
var timeout = duration * 1e3 / Math.abs(sound._rate);
|
|
595
|
+
var start = self._sprite[sprite][0] / 1e3;
|
|
596
|
+
var stop2 = (self._sprite[sprite][0] + self._sprite[sprite][1]) / 1e3;
|
|
597
|
+
sound._sprite = sprite;
|
|
598
|
+
sound._ended = false;
|
|
599
|
+
var setParams = function() {
|
|
600
|
+
sound._paused = false;
|
|
601
|
+
sound._seek = seek;
|
|
602
|
+
sound._start = start;
|
|
603
|
+
sound._stop = stop2;
|
|
604
|
+
sound._loop = !!(sound._loop || self._sprite[sprite][2]);
|
|
605
|
+
};
|
|
606
|
+
if (seek >= stop2) {
|
|
607
|
+
self._ended(sound);
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
var node = sound._node;
|
|
611
|
+
if (self._webAudio) {
|
|
612
|
+
var playWebAudio = function() {
|
|
613
|
+
self._playLock = false;
|
|
614
|
+
setParams();
|
|
615
|
+
self._refreshBuffer(sound);
|
|
616
|
+
var vol = sound._muted || self._muted ? 0 : sound._volume;
|
|
617
|
+
node.gain.setValueAtTime(vol, Howler2.ctx.currentTime);
|
|
618
|
+
sound._playStart = Howler2.ctx.currentTime;
|
|
619
|
+
if (typeof node.bufferSource.start === "undefined") {
|
|
620
|
+
sound._loop ? node.bufferSource.noteGrainOn(0, seek, 86400) : node.bufferSource.noteGrainOn(0, seek, duration);
|
|
621
|
+
} else {
|
|
622
|
+
sound._loop ? node.bufferSource.start(0, seek, 86400) : node.bufferSource.start(0, seek, duration);
|
|
623
|
+
}
|
|
624
|
+
if (timeout !== Infinity) {
|
|
625
|
+
self._endTimers[sound._id] = setTimeout(self._ended.bind(self, sound), timeout);
|
|
626
|
+
}
|
|
627
|
+
if (!internal) {
|
|
628
|
+
setTimeout(function() {
|
|
629
|
+
self._emit("play", sound._id);
|
|
630
|
+
self._loadQueue();
|
|
631
|
+
}, 0);
|
|
632
|
+
}
|
|
633
|
+
};
|
|
634
|
+
if (Howler2.state === "running" && Howler2.ctx.state !== "interrupted") {
|
|
635
|
+
playWebAudio();
|
|
636
|
+
} else {
|
|
637
|
+
self._playLock = true;
|
|
638
|
+
self.once("resume", playWebAudio);
|
|
639
|
+
self._clearTimer(sound._id);
|
|
640
|
+
}
|
|
641
|
+
} else {
|
|
642
|
+
var playHtml5 = function() {
|
|
643
|
+
node.currentTime = seek;
|
|
644
|
+
node.muted = sound._muted || self._muted || Howler2._muted || node.muted;
|
|
645
|
+
node.volume = sound._volume * Howler2.volume();
|
|
646
|
+
node.playbackRate = sound._rate;
|
|
647
|
+
try {
|
|
648
|
+
var play = node.play();
|
|
649
|
+
if (play && typeof Promise !== "undefined" && (play instanceof Promise || typeof play.then === "function")) {
|
|
650
|
+
self._playLock = true;
|
|
651
|
+
setParams();
|
|
652
|
+
play.then(function() {
|
|
653
|
+
self._playLock = false;
|
|
654
|
+
node._unlocked = true;
|
|
655
|
+
if (!internal) {
|
|
656
|
+
self._emit("play", sound._id);
|
|
657
|
+
} else {
|
|
658
|
+
self._loadQueue();
|
|
659
|
+
}
|
|
660
|
+
}).catch(function() {
|
|
661
|
+
self._playLock = false;
|
|
662
|
+
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.");
|
|
663
|
+
sound._ended = true;
|
|
664
|
+
sound._paused = true;
|
|
665
|
+
});
|
|
666
|
+
} else if (!internal) {
|
|
667
|
+
self._playLock = false;
|
|
668
|
+
setParams();
|
|
669
|
+
self._emit("play", sound._id);
|
|
670
|
+
}
|
|
671
|
+
node.playbackRate = sound._rate;
|
|
672
|
+
if (node.paused) {
|
|
673
|
+
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.");
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
if (sprite !== "__default" || sound._loop) {
|
|
677
|
+
self._endTimers[sound._id] = setTimeout(self._ended.bind(self, sound), timeout);
|
|
678
|
+
} else {
|
|
679
|
+
self._endTimers[sound._id] = function() {
|
|
680
|
+
self._ended(sound);
|
|
681
|
+
node.removeEventListener("ended", self._endTimers[sound._id], false);
|
|
682
|
+
};
|
|
683
|
+
node.addEventListener("ended", self._endTimers[sound._id], false);
|
|
684
|
+
}
|
|
685
|
+
} catch (err) {
|
|
686
|
+
self._emit("playerror", sound._id, err);
|
|
687
|
+
}
|
|
688
|
+
};
|
|
689
|
+
if (node.src === "data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA") {
|
|
690
|
+
node.src = self._src;
|
|
691
|
+
node.load();
|
|
692
|
+
}
|
|
693
|
+
var loadedNoReadyState = window && window.ejecta || !node.readyState && Howler2._navigator.isCocoonJS;
|
|
694
|
+
if (node.readyState >= 3 || loadedNoReadyState) {
|
|
695
|
+
playHtml5();
|
|
696
|
+
} else {
|
|
697
|
+
self._playLock = true;
|
|
698
|
+
self._state = "loading";
|
|
699
|
+
var listener = function() {
|
|
700
|
+
self._state = "loaded";
|
|
701
|
+
playHtml5();
|
|
702
|
+
node.removeEventListener(Howler2._canPlayEvent, listener, false);
|
|
703
|
+
};
|
|
704
|
+
node.addEventListener(Howler2._canPlayEvent, listener, false);
|
|
705
|
+
self._clearTimer(sound._id);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
return sound._id;
|
|
709
|
+
},
|
|
710
|
+
/**
|
|
711
|
+
* Pause playback and save current position.
|
|
712
|
+
* @param {Number} id The sound ID (empty to pause all in group).
|
|
713
|
+
* @return {Howl}
|
|
714
|
+
*/
|
|
715
|
+
pause: function(id) {
|
|
716
|
+
var self = this;
|
|
717
|
+
if (self._state !== "loaded" || self._playLock) {
|
|
718
|
+
self._queue.push({
|
|
719
|
+
event: "pause",
|
|
720
|
+
action: function() {
|
|
721
|
+
self.pause(id);
|
|
722
|
+
}
|
|
723
|
+
});
|
|
724
|
+
return self;
|
|
725
|
+
}
|
|
726
|
+
var ids = self._getSoundIds(id);
|
|
727
|
+
for (var i = 0; i < ids.length; i++) {
|
|
728
|
+
self._clearTimer(ids[i]);
|
|
729
|
+
var sound = self._soundById(ids[i]);
|
|
730
|
+
if (sound && !sound._paused) {
|
|
731
|
+
sound._seek = self.seek(ids[i]);
|
|
732
|
+
sound._rateSeek = 0;
|
|
733
|
+
sound._paused = true;
|
|
734
|
+
self._stopFade(ids[i]);
|
|
735
|
+
if (sound._node) {
|
|
736
|
+
if (self._webAudio) {
|
|
737
|
+
if (!sound._node.bufferSource) {
|
|
738
|
+
continue;
|
|
739
|
+
}
|
|
740
|
+
if (typeof sound._node.bufferSource.stop === "undefined") {
|
|
741
|
+
sound._node.bufferSource.noteOff(0);
|
|
742
|
+
} else {
|
|
743
|
+
sound._node.bufferSource.stop(0);
|
|
744
|
+
}
|
|
745
|
+
self._cleanBuffer(sound._node);
|
|
746
|
+
} else if (!isNaN(sound._node.duration) || sound._node.duration === Infinity) {
|
|
747
|
+
sound._node.pause();
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
if (!arguments[1]) {
|
|
752
|
+
self._emit("pause", sound ? sound._id : null);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
return self;
|
|
756
|
+
},
|
|
757
|
+
/**
|
|
758
|
+
* Stop playback and reset to start.
|
|
759
|
+
* @param {Number} id The sound ID (empty to stop all in group).
|
|
760
|
+
* @param {Boolean} internal Internal Use: true prevents event firing.
|
|
761
|
+
* @return {Howl}
|
|
762
|
+
*/
|
|
763
|
+
stop: function(id, internal) {
|
|
764
|
+
var self = this;
|
|
765
|
+
if (self._state !== "loaded" || self._playLock) {
|
|
766
|
+
self._queue.push({
|
|
767
|
+
event: "stop",
|
|
768
|
+
action: function() {
|
|
769
|
+
self.stop(id);
|
|
770
|
+
}
|
|
771
|
+
});
|
|
772
|
+
return self;
|
|
773
|
+
}
|
|
774
|
+
var ids = self._getSoundIds(id);
|
|
775
|
+
for (var i = 0; i < ids.length; i++) {
|
|
776
|
+
self._clearTimer(ids[i]);
|
|
777
|
+
var sound = self._soundById(ids[i]);
|
|
778
|
+
if (sound) {
|
|
779
|
+
sound._seek = sound._start || 0;
|
|
780
|
+
sound._rateSeek = 0;
|
|
781
|
+
sound._paused = true;
|
|
782
|
+
sound._ended = true;
|
|
783
|
+
self._stopFade(ids[i]);
|
|
784
|
+
if (sound._node) {
|
|
785
|
+
if (self._webAudio) {
|
|
786
|
+
if (sound._node.bufferSource) {
|
|
787
|
+
if (typeof sound._node.bufferSource.stop === "undefined") {
|
|
788
|
+
sound._node.bufferSource.noteOff(0);
|
|
789
|
+
} else {
|
|
790
|
+
sound._node.bufferSource.stop(0);
|
|
791
|
+
}
|
|
792
|
+
self._cleanBuffer(sound._node);
|
|
793
|
+
}
|
|
794
|
+
} else if (!isNaN(sound._node.duration) || sound._node.duration === Infinity) {
|
|
795
|
+
sound._node.currentTime = sound._start || 0;
|
|
796
|
+
sound._node.pause();
|
|
797
|
+
if (sound._node.duration === Infinity) {
|
|
798
|
+
self._clearSound(sound._node);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
if (!internal) {
|
|
803
|
+
self._emit("stop", sound._id);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
return self;
|
|
808
|
+
},
|
|
809
|
+
/**
|
|
810
|
+
* Mute/unmute a single sound or all sounds in this Howl group.
|
|
811
|
+
* @param {Boolean} muted Set to true to mute and false to unmute.
|
|
812
|
+
* @param {Number} id The sound ID to update (omit to mute/unmute all).
|
|
813
|
+
* @return {Howl}
|
|
814
|
+
*/
|
|
815
|
+
mute: function(muted, id) {
|
|
816
|
+
var self = this;
|
|
817
|
+
if (self._state !== "loaded" || self._playLock) {
|
|
818
|
+
self._queue.push({
|
|
819
|
+
event: "mute",
|
|
820
|
+
action: function() {
|
|
821
|
+
self.mute(muted, id);
|
|
822
|
+
}
|
|
823
|
+
});
|
|
824
|
+
return self;
|
|
825
|
+
}
|
|
826
|
+
if (typeof id === "undefined") {
|
|
827
|
+
if (typeof muted === "boolean") {
|
|
828
|
+
self._muted = muted;
|
|
829
|
+
} else {
|
|
830
|
+
return self._muted;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
var ids = self._getSoundIds(id);
|
|
834
|
+
for (var i = 0; i < ids.length; i++) {
|
|
835
|
+
var sound = self._soundById(ids[i]);
|
|
836
|
+
if (sound) {
|
|
837
|
+
sound._muted = muted;
|
|
838
|
+
if (sound._interval) {
|
|
839
|
+
self._stopFade(sound._id);
|
|
840
|
+
}
|
|
841
|
+
if (self._webAudio && sound._node) {
|
|
842
|
+
sound._node.gain.setValueAtTime(muted ? 0 : sound._volume, Howler2.ctx.currentTime);
|
|
843
|
+
} else if (sound._node) {
|
|
844
|
+
sound._node.muted = Howler2._muted ? true : muted;
|
|
845
|
+
}
|
|
846
|
+
self._emit("mute", sound._id);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
return self;
|
|
850
|
+
},
|
|
851
|
+
/**
|
|
852
|
+
* Get/set the volume of this sound or of the Howl group. This method can optionally take 0, 1 or 2 arguments.
|
|
853
|
+
* volume() -> Returns the group's volume value.
|
|
854
|
+
* volume(id) -> Returns the sound id's current volume.
|
|
855
|
+
* volume(vol) -> Sets the volume of all sounds in this Howl group.
|
|
856
|
+
* volume(vol, id) -> Sets the volume of passed sound id.
|
|
857
|
+
* @return {Howl/Number} Returns self or current volume.
|
|
858
|
+
*/
|
|
859
|
+
volume: function() {
|
|
860
|
+
var self = this;
|
|
861
|
+
var args = arguments;
|
|
862
|
+
var vol, id;
|
|
863
|
+
if (args.length === 0) {
|
|
864
|
+
return self._volume;
|
|
865
|
+
} else if (args.length === 1 || args.length === 2 && typeof args[1] === "undefined") {
|
|
866
|
+
var ids = self._getSoundIds();
|
|
867
|
+
var index = ids.indexOf(args[0]);
|
|
868
|
+
if (index >= 0) {
|
|
869
|
+
id = parseInt(args[0], 10);
|
|
870
|
+
} else {
|
|
871
|
+
vol = parseFloat(args[0]);
|
|
872
|
+
}
|
|
873
|
+
} else if (args.length >= 2) {
|
|
874
|
+
vol = parseFloat(args[0]);
|
|
875
|
+
id = parseInt(args[1], 10);
|
|
876
|
+
}
|
|
877
|
+
var sound;
|
|
878
|
+
if (typeof vol !== "undefined" && vol >= 0 && vol <= 1) {
|
|
879
|
+
if (self._state !== "loaded" || self._playLock) {
|
|
880
|
+
self._queue.push({
|
|
881
|
+
event: "volume",
|
|
882
|
+
action: function() {
|
|
883
|
+
self.volume.apply(self, args);
|
|
884
|
+
}
|
|
885
|
+
});
|
|
886
|
+
return self;
|
|
887
|
+
}
|
|
888
|
+
if (typeof id === "undefined") {
|
|
889
|
+
self._volume = vol;
|
|
890
|
+
}
|
|
891
|
+
id = self._getSoundIds(id);
|
|
892
|
+
for (var i = 0; i < id.length; i++) {
|
|
893
|
+
sound = self._soundById(id[i]);
|
|
894
|
+
if (sound) {
|
|
895
|
+
sound._volume = vol;
|
|
896
|
+
if (!args[2]) {
|
|
897
|
+
self._stopFade(id[i]);
|
|
898
|
+
}
|
|
899
|
+
if (self._webAudio && sound._node && !sound._muted) {
|
|
900
|
+
sound._node.gain.setValueAtTime(vol, Howler2.ctx.currentTime);
|
|
901
|
+
} else if (sound._node && !sound._muted) {
|
|
902
|
+
sound._node.volume = vol * Howler2.volume();
|
|
903
|
+
}
|
|
904
|
+
self._emit("volume", sound._id);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
} else {
|
|
908
|
+
sound = id ? self._soundById(id) : self._sounds[0];
|
|
909
|
+
return sound ? sound._volume : 0;
|
|
910
|
+
}
|
|
911
|
+
return self;
|
|
912
|
+
},
|
|
913
|
+
/**
|
|
914
|
+
* Fade a currently playing sound between two volumes (if no id is passed, all sounds will fade).
|
|
915
|
+
* @param {Number} from The value to fade from (0.0 to 1.0).
|
|
916
|
+
* @param {Number} to The volume to fade to (0.0 to 1.0).
|
|
917
|
+
* @param {Number} len Time in milliseconds to fade.
|
|
918
|
+
* @param {Number} id The sound id (omit to fade all sounds).
|
|
919
|
+
* @return {Howl}
|
|
920
|
+
*/
|
|
921
|
+
fade: function(from, to, len, id) {
|
|
922
|
+
var self = this;
|
|
923
|
+
if (self._state !== "loaded" || self._playLock) {
|
|
924
|
+
self._queue.push({
|
|
925
|
+
event: "fade",
|
|
926
|
+
action: function() {
|
|
927
|
+
self.fade(from, to, len, id);
|
|
928
|
+
}
|
|
929
|
+
});
|
|
930
|
+
return self;
|
|
931
|
+
}
|
|
932
|
+
from = Math.min(Math.max(0, parseFloat(from)), 1);
|
|
933
|
+
to = Math.min(Math.max(0, parseFloat(to)), 1);
|
|
934
|
+
len = parseFloat(len);
|
|
935
|
+
self.volume(from, id);
|
|
936
|
+
var ids = self._getSoundIds(id);
|
|
937
|
+
for (var i = 0; i < ids.length; i++) {
|
|
938
|
+
var sound = self._soundById(ids[i]);
|
|
939
|
+
if (sound) {
|
|
940
|
+
if (!id) {
|
|
941
|
+
self._stopFade(ids[i]);
|
|
942
|
+
}
|
|
943
|
+
if (self._webAudio && !sound._muted) {
|
|
944
|
+
var currentTime = Howler2.ctx.currentTime;
|
|
945
|
+
var end = currentTime + len / 1e3;
|
|
946
|
+
sound._volume = from;
|
|
947
|
+
sound._node.gain.setValueAtTime(from, currentTime);
|
|
948
|
+
sound._node.gain.linearRampToValueAtTime(to, end);
|
|
949
|
+
}
|
|
950
|
+
self._startFadeInterval(sound, from, to, len, ids[i], typeof id === "undefined");
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
return self;
|
|
954
|
+
},
|
|
955
|
+
/**
|
|
956
|
+
* Starts the internal interval to fade a sound.
|
|
957
|
+
* @param {Object} sound Reference to sound to fade.
|
|
958
|
+
* @param {Number} from The value to fade from (0.0 to 1.0).
|
|
959
|
+
* @param {Number} to The volume to fade to (0.0 to 1.0).
|
|
960
|
+
* @param {Number} len Time in milliseconds to fade.
|
|
961
|
+
* @param {Number} id The sound id to fade.
|
|
962
|
+
* @param {Boolean} isGroup If true, set the volume on the group.
|
|
963
|
+
*/
|
|
964
|
+
_startFadeInterval: function(sound, from, to, len, id, isGroup) {
|
|
965
|
+
var self = this;
|
|
966
|
+
var vol = from;
|
|
967
|
+
var diff = to - from;
|
|
968
|
+
var steps = Math.abs(diff / 0.01);
|
|
969
|
+
var stepLen = Math.max(4, steps > 0 ? len / steps : len);
|
|
970
|
+
var lastTick = Date.now();
|
|
971
|
+
sound._fadeTo = to;
|
|
972
|
+
sound._interval = setInterval(function() {
|
|
973
|
+
var tick = (Date.now() - lastTick) / len;
|
|
974
|
+
lastTick = Date.now();
|
|
975
|
+
vol += diff * tick;
|
|
976
|
+
vol = Math.round(vol * 100) / 100;
|
|
977
|
+
if (diff < 0) {
|
|
978
|
+
vol = Math.max(to, vol);
|
|
979
|
+
} else {
|
|
980
|
+
vol = Math.min(to, vol);
|
|
981
|
+
}
|
|
982
|
+
if (self._webAudio) {
|
|
983
|
+
sound._volume = vol;
|
|
984
|
+
} else {
|
|
985
|
+
self.volume(vol, sound._id, true);
|
|
986
|
+
}
|
|
987
|
+
if (isGroup) {
|
|
988
|
+
self._volume = vol;
|
|
989
|
+
}
|
|
990
|
+
if (to < from && vol <= to || to > from && vol >= to) {
|
|
991
|
+
clearInterval(sound._interval);
|
|
992
|
+
sound._interval = null;
|
|
993
|
+
sound._fadeTo = null;
|
|
994
|
+
self.volume(to, sound._id);
|
|
995
|
+
self._emit("fade", sound._id);
|
|
996
|
+
}
|
|
997
|
+
}, stepLen);
|
|
998
|
+
},
|
|
999
|
+
/**
|
|
1000
|
+
* Internal method that stops the currently playing fade when
|
|
1001
|
+
* a new fade starts, volume is changed or the sound is stopped.
|
|
1002
|
+
* @param {Number} id The sound id.
|
|
1003
|
+
* @return {Howl}
|
|
1004
|
+
*/
|
|
1005
|
+
_stopFade: function(id) {
|
|
1006
|
+
var self = this;
|
|
1007
|
+
var sound = self._soundById(id);
|
|
1008
|
+
if (sound && sound._interval) {
|
|
1009
|
+
if (self._webAudio) {
|
|
1010
|
+
sound._node.gain.cancelScheduledValues(Howler2.ctx.currentTime);
|
|
1011
|
+
}
|
|
1012
|
+
clearInterval(sound._interval);
|
|
1013
|
+
sound._interval = null;
|
|
1014
|
+
self.volume(sound._fadeTo, id);
|
|
1015
|
+
sound._fadeTo = null;
|
|
1016
|
+
self._emit("fade", id);
|
|
1017
|
+
}
|
|
1018
|
+
return self;
|
|
1019
|
+
},
|
|
1020
|
+
/**
|
|
1021
|
+
* Get/set the loop parameter on a sound. This method can optionally take 0, 1 or 2 arguments.
|
|
1022
|
+
* loop() -> Returns the group's loop value.
|
|
1023
|
+
* loop(id) -> Returns the sound id's loop value.
|
|
1024
|
+
* loop(loop) -> Sets the loop value for all sounds in this Howl group.
|
|
1025
|
+
* loop(loop, id) -> Sets the loop value of passed sound id.
|
|
1026
|
+
* @return {Howl/Boolean} Returns self or current loop value.
|
|
1027
|
+
*/
|
|
1028
|
+
loop: function() {
|
|
1029
|
+
var self = this;
|
|
1030
|
+
var args = arguments;
|
|
1031
|
+
var loop, id, sound;
|
|
1032
|
+
if (args.length === 0) {
|
|
1033
|
+
return self._loop;
|
|
1034
|
+
} else if (args.length === 1) {
|
|
1035
|
+
if (typeof args[0] === "boolean") {
|
|
1036
|
+
loop = args[0];
|
|
1037
|
+
self._loop = loop;
|
|
1038
|
+
} else {
|
|
1039
|
+
sound = self._soundById(parseInt(args[0], 10));
|
|
1040
|
+
return sound ? sound._loop : false;
|
|
1041
|
+
}
|
|
1042
|
+
} else if (args.length === 2) {
|
|
1043
|
+
loop = args[0];
|
|
1044
|
+
id = parseInt(args[1], 10);
|
|
1045
|
+
}
|
|
1046
|
+
var ids = self._getSoundIds(id);
|
|
1047
|
+
for (var i = 0; i < ids.length; i++) {
|
|
1048
|
+
sound = self._soundById(ids[i]);
|
|
1049
|
+
if (sound) {
|
|
1050
|
+
sound._loop = loop;
|
|
1051
|
+
if (self._webAudio && sound._node && sound._node.bufferSource) {
|
|
1052
|
+
sound._node.bufferSource.loop = loop;
|
|
1053
|
+
if (loop) {
|
|
1054
|
+
sound._node.bufferSource.loopStart = sound._start || 0;
|
|
1055
|
+
sound._node.bufferSource.loopEnd = sound._stop;
|
|
1056
|
+
if (self.playing(ids[i])) {
|
|
1057
|
+
self.pause(ids[i], true);
|
|
1058
|
+
self.play(ids[i], true);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
return self;
|
|
1065
|
+
},
|
|
1066
|
+
/**
|
|
1067
|
+
* Get/set the playback rate of a sound. This method can optionally take 0, 1 or 2 arguments.
|
|
1068
|
+
* rate() -> Returns the first sound node's current playback rate.
|
|
1069
|
+
* rate(id) -> Returns the sound id's current playback rate.
|
|
1070
|
+
* rate(rate) -> Sets the playback rate of all sounds in this Howl group.
|
|
1071
|
+
* rate(rate, id) -> Sets the playback rate of passed sound id.
|
|
1072
|
+
* @return {Howl/Number} Returns self or the current playback rate.
|
|
1073
|
+
*/
|
|
1074
|
+
rate: function() {
|
|
1075
|
+
var self = this;
|
|
1076
|
+
var args = arguments;
|
|
1077
|
+
var rate, id;
|
|
1078
|
+
if (args.length === 0) {
|
|
1079
|
+
id = self._sounds[0]._id;
|
|
1080
|
+
} else if (args.length === 1) {
|
|
1081
|
+
var ids = self._getSoundIds();
|
|
1082
|
+
var index = ids.indexOf(args[0]);
|
|
1083
|
+
if (index >= 0) {
|
|
1084
|
+
id = parseInt(args[0], 10);
|
|
1085
|
+
} else {
|
|
1086
|
+
rate = parseFloat(args[0]);
|
|
1087
|
+
}
|
|
1088
|
+
} else if (args.length === 2) {
|
|
1089
|
+
rate = parseFloat(args[0]);
|
|
1090
|
+
id = parseInt(args[1], 10);
|
|
1091
|
+
}
|
|
1092
|
+
var sound;
|
|
1093
|
+
if (typeof rate === "number") {
|
|
1094
|
+
if (self._state !== "loaded" || self._playLock) {
|
|
1095
|
+
self._queue.push({
|
|
1096
|
+
event: "rate",
|
|
1097
|
+
action: function() {
|
|
1098
|
+
self.rate.apply(self, args);
|
|
1099
|
+
}
|
|
1100
|
+
});
|
|
1101
|
+
return self;
|
|
1102
|
+
}
|
|
1103
|
+
if (typeof id === "undefined") {
|
|
1104
|
+
self._rate = rate;
|
|
1105
|
+
}
|
|
1106
|
+
id = self._getSoundIds(id);
|
|
1107
|
+
for (var i = 0; i < id.length; i++) {
|
|
1108
|
+
sound = self._soundById(id[i]);
|
|
1109
|
+
if (sound) {
|
|
1110
|
+
if (self.playing(id[i])) {
|
|
1111
|
+
sound._rateSeek = self.seek(id[i]);
|
|
1112
|
+
sound._playStart = self._webAudio ? Howler2.ctx.currentTime : sound._playStart;
|
|
1113
|
+
}
|
|
1114
|
+
sound._rate = rate;
|
|
1115
|
+
if (self._webAudio && sound._node && sound._node.bufferSource) {
|
|
1116
|
+
sound._node.bufferSource.playbackRate.setValueAtTime(rate, Howler2.ctx.currentTime);
|
|
1117
|
+
} else if (sound._node) {
|
|
1118
|
+
sound._node.playbackRate = rate;
|
|
1119
|
+
}
|
|
1120
|
+
var seek = self.seek(id[i]);
|
|
1121
|
+
var duration = (self._sprite[sound._sprite][0] + self._sprite[sound._sprite][1]) / 1e3 - seek;
|
|
1122
|
+
var timeout = duration * 1e3 / Math.abs(sound._rate);
|
|
1123
|
+
if (self._endTimers[id[i]] || !sound._paused) {
|
|
1124
|
+
self._clearTimer(id[i]);
|
|
1125
|
+
self._endTimers[id[i]] = setTimeout(self._ended.bind(self, sound), timeout);
|
|
1126
|
+
}
|
|
1127
|
+
self._emit("rate", sound._id);
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
} else {
|
|
1131
|
+
sound = self._soundById(id);
|
|
1132
|
+
return sound ? sound._rate : self._rate;
|
|
1133
|
+
}
|
|
1134
|
+
return self;
|
|
1135
|
+
},
|
|
1136
|
+
/**
|
|
1137
|
+
* Get/set the seek position of a sound. This method can optionally take 0, 1 or 2 arguments.
|
|
1138
|
+
* seek() -> Returns the first sound node's current seek position.
|
|
1139
|
+
* seek(id) -> Returns the sound id's current seek position.
|
|
1140
|
+
* seek(seek) -> Sets the seek position of the first sound node.
|
|
1141
|
+
* seek(seek, id) -> Sets the seek position of passed sound id.
|
|
1142
|
+
* @return {Howl/Number} Returns self or the current seek position.
|
|
1143
|
+
*/
|
|
1144
|
+
seek: function() {
|
|
1145
|
+
var self = this;
|
|
1146
|
+
var args = arguments;
|
|
1147
|
+
var seek, id;
|
|
1148
|
+
if (args.length === 0) {
|
|
1149
|
+
if (self._sounds.length) {
|
|
1150
|
+
id = self._sounds[0]._id;
|
|
1151
|
+
}
|
|
1152
|
+
} else if (args.length === 1) {
|
|
1153
|
+
var ids = self._getSoundIds();
|
|
1154
|
+
var index = ids.indexOf(args[0]);
|
|
1155
|
+
if (index >= 0) {
|
|
1156
|
+
id = parseInt(args[0], 10);
|
|
1157
|
+
} else if (self._sounds.length) {
|
|
1158
|
+
id = self._sounds[0]._id;
|
|
1159
|
+
seek = parseFloat(args[0]);
|
|
1160
|
+
}
|
|
1161
|
+
} else if (args.length === 2) {
|
|
1162
|
+
seek = parseFloat(args[0]);
|
|
1163
|
+
id = parseInt(args[1], 10);
|
|
1164
|
+
}
|
|
1165
|
+
if (typeof id === "undefined") {
|
|
1166
|
+
return 0;
|
|
1167
|
+
}
|
|
1168
|
+
if (typeof seek === "number" && (self._state !== "loaded" || self._playLock)) {
|
|
1169
|
+
self._queue.push({
|
|
1170
|
+
event: "seek",
|
|
1171
|
+
action: function() {
|
|
1172
|
+
self.seek.apply(self, args);
|
|
1173
|
+
}
|
|
1174
|
+
});
|
|
1175
|
+
return self;
|
|
1176
|
+
}
|
|
1177
|
+
var sound = self._soundById(id);
|
|
1178
|
+
if (sound) {
|
|
1179
|
+
if (typeof seek === "number" && seek >= 0) {
|
|
1180
|
+
var playing = self.playing(id);
|
|
1181
|
+
if (playing) {
|
|
1182
|
+
self.pause(id, true);
|
|
1183
|
+
}
|
|
1184
|
+
sound._seek = seek;
|
|
1185
|
+
sound._ended = false;
|
|
1186
|
+
self._clearTimer(id);
|
|
1187
|
+
if (!self._webAudio && sound._node && !isNaN(sound._node.duration)) {
|
|
1188
|
+
sound._node.currentTime = seek;
|
|
1189
|
+
}
|
|
1190
|
+
var seekAndEmit = function() {
|
|
1191
|
+
if (playing) {
|
|
1192
|
+
self.play(id, true);
|
|
1193
|
+
}
|
|
1194
|
+
self._emit("seek", id);
|
|
1195
|
+
};
|
|
1196
|
+
if (playing && !self._webAudio) {
|
|
1197
|
+
var emitSeek = function() {
|
|
1198
|
+
if (!self._playLock) {
|
|
1199
|
+
seekAndEmit();
|
|
1200
|
+
} else {
|
|
1201
|
+
setTimeout(emitSeek, 0);
|
|
1202
|
+
}
|
|
1203
|
+
};
|
|
1204
|
+
setTimeout(emitSeek, 0);
|
|
1205
|
+
} else {
|
|
1206
|
+
seekAndEmit();
|
|
1207
|
+
}
|
|
1208
|
+
} else {
|
|
1209
|
+
if (self._webAudio) {
|
|
1210
|
+
var realTime = self.playing(id) ? Howler2.ctx.currentTime - sound._playStart : 0;
|
|
1211
|
+
var rateSeek = sound._rateSeek ? sound._rateSeek - sound._seek : 0;
|
|
1212
|
+
return sound._seek + (rateSeek + realTime * Math.abs(sound._rate));
|
|
1213
|
+
} else {
|
|
1214
|
+
return sound._node.currentTime;
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
return self;
|
|
1219
|
+
},
|
|
1220
|
+
/**
|
|
1221
|
+
* 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.
|
|
1222
|
+
* @param {Number} id The sound id to check. If none is passed, the whole sound group is checked.
|
|
1223
|
+
* @return {Boolean} True if playing and false if not.
|
|
1224
|
+
*/
|
|
1225
|
+
playing: function(id) {
|
|
1226
|
+
var self = this;
|
|
1227
|
+
if (typeof id === "number") {
|
|
1228
|
+
var sound = self._soundById(id);
|
|
1229
|
+
return sound ? !sound._paused : false;
|
|
1230
|
+
}
|
|
1231
|
+
for (var i = 0; i < self._sounds.length; i++) {
|
|
1232
|
+
if (!self._sounds[i]._paused) {
|
|
1233
|
+
return true;
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
return false;
|
|
1237
|
+
},
|
|
1238
|
+
/**
|
|
1239
|
+
* Get the duration of this sound. Passing a sound id will return the sprite duration.
|
|
1240
|
+
* @param {Number} id The sound id to check. If none is passed, return full source duration.
|
|
1241
|
+
* @return {Number} Audio duration in seconds.
|
|
1242
|
+
*/
|
|
1243
|
+
duration: function(id) {
|
|
1244
|
+
var self = this;
|
|
1245
|
+
var duration = self._duration;
|
|
1246
|
+
var sound = self._soundById(id);
|
|
1247
|
+
if (sound) {
|
|
1248
|
+
duration = self._sprite[sound._sprite][1] / 1e3;
|
|
1249
|
+
}
|
|
1250
|
+
return duration;
|
|
1251
|
+
},
|
|
1252
|
+
/**
|
|
1253
|
+
* Returns the current loaded state of this Howl.
|
|
1254
|
+
* @return {String} 'unloaded', 'loading', 'loaded'
|
|
1255
|
+
*/
|
|
1256
|
+
state: function() {
|
|
1257
|
+
return this._state;
|
|
1258
|
+
},
|
|
1259
|
+
/**
|
|
1260
|
+
* Unload and destroy the current Howl object.
|
|
1261
|
+
* This will immediately stop all sound instances attached to this group.
|
|
1262
|
+
*/
|
|
1263
|
+
unload: function() {
|
|
1264
|
+
var self = this;
|
|
1265
|
+
var sounds = self._sounds;
|
|
1266
|
+
for (var i = 0; i < sounds.length; i++) {
|
|
1267
|
+
if (!sounds[i]._paused) {
|
|
1268
|
+
self.stop(sounds[i]._id);
|
|
1269
|
+
}
|
|
1270
|
+
if (!self._webAudio) {
|
|
1271
|
+
self._clearSound(sounds[i]._node);
|
|
1272
|
+
sounds[i]._node.removeEventListener("error", sounds[i]._errorFn, false);
|
|
1273
|
+
sounds[i]._node.removeEventListener(Howler2._canPlayEvent, sounds[i]._loadFn, false);
|
|
1274
|
+
sounds[i]._node.removeEventListener("ended", sounds[i]._endFn, false);
|
|
1275
|
+
Howler2._releaseHtml5Audio(sounds[i]._node);
|
|
1276
|
+
}
|
|
1277
|
+
delete sounds[i]._node;
|
|
1278
|
+
self._clearTimer(sounds[i]._id);
|
|
1279
|
+
}
|
|
1280
|
+
var index = Howler2._howls.indexOf(self);
|
|
1281
|
+
if (index >= 0) {
|
|
1282
|
+
Howler2._howls.splice(index, 1);
|
|
1283
|
+
}
|
|
1284
|
+
var remCache = true;
|
|
1285
|
+
for (i = 0; i < Howler2._howls.length; i++) {
|
|
1286
|
+
if (Howler2._howls[i]._src === self._src || self._src.indexOf(Howler2._howls[i]._src) >= 0) {
|
|
1287
|
+
remCache = false;
|
|
1288
|
+
break;
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
if (cache && remCache) {
|
|
1292
|
+
delete cache[self._src];
|
|
1293
|
+
}
|
|
1294
|
+
Howler2.noAudio = false;
|
|
1295
|
+
self._state = "unloaded";
|
|
1296
|
+
self._sounds = [];
|
|
1297
|
+
self = null;
|
|
1298
|
+
return null;
|
|
1299
|
+
},
|
|
1300
|
+
/**
|
|
1301
|
+
* Listen to a custom event.
|
|
1302
|
+
* @param {String} event Event name.
|
|
1303
|
+
* @param {Function} fn Listener to call.
|
|
1304
|
+
* @param {Number} id (optional) Only listen to events for this sound.
|
|
1305
|
+
* @param {Number} once (INTERNAL) Marks event to fire only once.
|
|
1306
|
+
* @return {Howl}
|
|
1307
|
+
*/
|
|
1308
|
+
on: function(event, fn, id, once) {
|
|
1309
|
+
var self = this;
|
|
1310
|
+
var events = self["_on" + event];
|
|
1311
|
+
if (typeof fn === "function") {
|
|
1312
|
+
events.push(once ? { id, fn, once } : { id, fn });
|
|
1313
|
+
}
|
|
1314
|
+
return self;
|
|
1315
|
+
},
|
|
1316
|
+
/**
|
|
1317
|
+
* Remove a custom event. Call without parameters to remove all events.
|
|
1318
|
+
* @param {String} event Event name.
|
|
1319
|
+
* @param {Function} fn Listener to remove. Leave empty to remove all.
|
|
1320
|
+
* @param {Number} id (optional) Only remove events for this sound.
|
|
1321
|
+
* @return {Howl}
|
|
1322
|
+
*/
|
|
1323
|
+
off: function(event, fn, id) {
|
|
1324
|
+
var self = this;
|
|
1325
|
+
var events = self["_on" + event];
|
|
1326
|
+
var i = 0;
|
|
1327
|
+
if (typeof fn === "number") {
|
|
1328
|
+
id = fn;
|
|
1329
|
+
fn = null;
|
|
1330
|
+
}
|
|
1331
|
+
if (fn || id) {
|
|
1332
|
+
for (i = 0; i < events.length; i++) {
|
|
1333
|
+
var isId = id === events[i].id;
|
|
1334
|
+
if (fn === events[i].fn && isId || !fn && isId) {
|
|
1335
|
+
events.splice(i, 1);
|
|
1336
|
+
break;
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
} else if (event) {
|
|
1340
|
+
self["_on" + event] = [];
|
|
1341
|
+
} else {
|
|
1342
|
+
var keys = Object.keys(self);
|
|
1343
|
+
for (i = 0; i < keys.length; i++) {
|
|
1344
|
+
if (keys[i].indexOf("_on") === 0 && Array.isArray(self[keys[i]])) {
|
|
1345
|
+
self[keys[i]] = [];
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
return self;
|
|
1350
|
+
},
|
|
1351
|
+
/**
|
|
1352
|
+
* Listen to a custom event and remove it once fired.
|
|
1353
|
+
* @param {String} event Event name.
|
|
1354
|
+
* @param {Function} fn Listener to call.
|
|
1355
|
+
* @param {Number} id (optional) Only listen to events for this sound.
|
|
1356
|
+
* @return {Howl}
|
|
1357
|
+
*/
|
|
1358
|
+
once: function(event, fn, id) {
|
|
1359
|
+
var self = this;
|
|
1360
|
+
self.on(event, fn, id, 1);
|
|
1361
|
+
return self;
|
|
1362
|
+
},
|
|
1363
|
+
/**
|
|
1364
|
+
* Emit all events of a specific type and pass the sound id.
|
|
1365
|
+
* @param {String} event Event name.
|
|
1366
|
+
* @param {Number} id Sound ID.
|
|
1367
|
+
* @param {Number} msg Message to go with event.
|
|
1368
|
+
* @return {Howl}
|
|
1369
|
+
*/
|
|
1370
|
+
_emit: function(event, id, msg) {
|
|
1371
|
+
var self = this;
|
|
1372
|
+
var events = self["_on" + event];
|
|
1373
|
+
for (var i = events.length - 1; i >= 0; i--) {
|
|
1374
|
+
if (!events[i].id || events[i].id === id || event === "load") {
|
|
1375
|
+
setTimeout(function(fn) {
|
|
1376
|
+
fn.call(this, id, msg);
|
|
1377
|
+
}.bind(self, events[i].fn), 0);
|
|
1378
|
+
if (events[i].once) {
|
|
1379
|
+
self.off(event, events[i].fn, events[i].id);
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
self._loadQueue(event);
|
|
1384
|
+
return self;
|
|
1385
|
+
},
|
|
1386
|
+
/**
|
|
1387
|
+
* Queue of actions initiated before the sound has loaded.
|
|
1388
|
+
* These will be called in sequence, with the next only firing
|
|
1389
|
+
* after the previous has finished executing (even if async like play).
|
|
1390
|
+
* @return {Howl}
|
|
1391
|
+
*/
|
|
1392
|
+
_loadQueue: function(event) {
|
|
1393
|
+
var self = this;
|
|
1394
|
+
if (self._queue.length > 0) {
|
|
1395
|
+
var task = self._queue[0];
|
|
1396
|
+
if (task.event === event) {
|
|
1397
|
+
self._queue.shift();
|
|
1398
|
+
self._loadQueue();
|
|
1399
|
+
}
|
|
1400
|
+
if (!event) {
|
|
1401
|
+
task.action();
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
return self;
|
|
1405
|
+
},
|
|
1406
|
+
/**
|
|
1407
|
+
* Fired when playback ends at the end of the duration.
|
|
1408
|
+
* @param {Sound} sound The sound object to work with.
|
|
1409
|
+
* @return {Howl}
|
|
1410
|
+
*/
|
|
1411
|
+
_ended: function(sound) {
|
|
1412
|
+
var self = this;
|
|
1413
|
+
var sprite = sound._sprite;
|
|
1414
|
+
if (!self._webAudio && sound._node && !sound._node.paused && !sound._node.ended && sound._node.currentTime < sound._stop) {
|
|
1415
|
+
setTimeout(self._ended.bind(self, sound), 100);
|
|
1416
|
+
return self;
|
|
1417
|
+
}
|
|
1418
|
+
var loop = !!(sound._loop || self._sprite[sprite][2]);
|
|
1419
|
+
self._emit("end", sound._id);
|
|
1420
|
+
if (!self._webAudio && loop) {
|
|
1421
|
+
self.stop(sound._id, true).play(sound._id);
|
|
1422
|
+
}
|
|
1423
|
+
if (self._webAudio && loop) {
|
|
1424
|
+
self._emit("play", sound._id);
|
|
1425
|
+
sound._seek = sound._start || 0;
|
|
1426
|
+
sound._rateSeek = 0;
|
|
1427
|
+
sound._playStart = Howler2.ctx.currentTime;
|
|
1428
|
+
var timeout = (sound._stop - sound._start) * 1e3 / Math.abs(sound._rate);
|
|
1429
|
+
self._endTimers[sound._id] = setTimeout(self._ended.bind(self, sound), timeout);
|
|
1430
|
+
}
|
|
1431
|
+
if (self._webAudio && !loop) {
|
|
1432
|
+
sound._paused = true;
|
|
1433
|
+
sound._ended = true;
|
|
1434
|
+
sound._seek = sound._start || 0;
|
|
1435
|
+
sound._rateSeek = 0;
|
|
1436
|
+
self._clearTimer(sound._id);
|
|
1437
|
+
self._cleanBuffer(sound._node);
|
|
1438
|
+
Howler2._autoSuspend();
|
|
1439
|
+
}
|
|
1440
|
+
if (!self._webAudio && !loop) {
|
|
1441
|
+
self.stop(sound._id, true);
|
|
1442
|
+
}
|
|
1443
|
+
return self;
|
|
1444
|
+
},
|
|
1445
|
+
/**
|
|
1446
|
+
* Clear the end timer for a sound playback.
|
|
1447
|
+
* @param {Number} id The sound ID.
|
|
1448
|
+
* @return {Howl}
|
|
1449
|
+
*/
|
|
1450
|
+
_clearTimer: function(id) {
|
|
1451
|
+
var self = this;
|
|
1452
|
+
if (self._endTimers[id]) {
|
|
1453
|
+
if (typeof self._endTimers[id] !== "function") {
|
|
1454
|
+
clearTimeout(self._endTimers[id]);
|
|
1455
|
+
} else {
|
|
1456
|
+
var sound = self._soundById(id);
|
|
1457
|
+
if (sound && sound._node) {
|
|
1458
|
+
sound._node.removeEventListener("ended", self._endTimers[id], false);
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
delete self._endTimers[id];
|
|
1462
|
+
}
|
|
1463
|
+
return self;
|
|
1464
|
+
},
|
|
1465
|
+
/**
|
|
1466
|
+
* Return the sound identified by this ID, or return null.
|
|
1467
|
+
* @param {Number} id Sound ID
|
|
1468
|
+
* @return {Object} Sound object or null.
|
|
1469
|
+
*/
|
|
1470
|
+
_soundById: function(id) {
|
|
1471
|
+
var self = this;
|
|
1472
|
+
for (var i = 0; i < self._sounds.length; i++) {
|
|
1473
|
+
if (id === self._sounds[i]._id) {
|
|
1474
|
+
return self._sounds[i];
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
return null;
|
|
1478
|
+
},
|
|
1479
|
+
/**
|
|
1480
|
+
* Return an inactive sound from the pool or create a new one.
|
|
1481
|
+
* @return {Sound} Sound playback object.
|
|
1482
|
+
*/
|
|
1483
|
+
_inactiveSound: function() {
|
|
1484
|
+
var self = this;
|
|
1485
|
+
self._drain();
|
|
1486
|
+
for (var i = 0; i < self._sounds.length; i++) {
|
|
1487
|
+
if (self._sounds[i]._ended) {
|
|
1488
|
+
return self._sounds[i].reset();
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
return new Sound2(self);
|
|
1492
|
+
},
|
|
1493
|
+
/**
|
|
1494
|
+
* Drain excess inactive sounds from the pool.
|
|
1495
|
+
*/
|
|
1496
|
+
_drain: function() {
|
|
1497
|
+
var self = this;
|
|
1498
|
+
var limit = self._pool;
|
|
1499
|
+
var cnt = 0;
|
|
1500
|
+
var i = 0;
|
|
1501
|
+
if (self._sounds.length < limit) {
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1504
|
+
for (i = 0; i < self._sounds.length; i++) {
|
|
1505
|
+
if (self._sounds[i]._ended) {
|
|
1506
|
+
cnt++;
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
for (i = self._sounds.length - 1; i >= 0; i--) {
|
|
1510
|
+
if (cnt <= limit) {
|
|
1511
|
+
return;
|
|
1512
|
+
}
|
|
1513
|
+
if (self._sounds[i]._ended) {
|
|
1514
|
+
if (self._webAudio && self._sounds[i]._node) {
|
|
1515
|
+
self._sounds[i]._node.disconnect(0);
|
|
1516
|
+
}
|
|
1517
|
+
self._sounds.splice(i, 1);
|
|
1518
|
+
cnt--;
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
},
|
|
1522
|
+
/**
|
|
1523
|
+
* Get all ID's from the sounds pool.
|
|
1524
|
+
* @param {Number} id Only return one ID if one is passed.
|
|
1525
|
+
* @return {Array} Array of IDs.
|
|
1526
|
+
*/
|
|
1527
|
+
_getSoundIds: function(id) {
|
|
1528
|
+
var self = this;
|
|
1529
|
+
if (typeof id === "undefined") {
|
|
1530
|
+
var ids = [];
|
|
1531
|
+
for (var i = 0; i < self._sounds.length; i++) {
|
|
1532
|
+
ids.push(self._sounds[i]._id);
|
|
1533
|
+
}
|
|
1534
|
+
return ids;
|
|
1535
|
+
} else {
|
|
1536
|
+
return [id];
|
|
1537
|
+
}
|
|
1538
|
+
},
|
|
1539
|
+
/**
|
|
1540
|
+
* Load the sound back into the buffer source.
|
|
1541
|
+
* @param {Sound} sound The sound object to work with.
|
|
1542
|
+
* @return {Howl}
|
|
1543
|
+
*/
|
|
1544
|
+
_refreshBuffer: function(sound) {
|
|
1545
|
+
var self = this;
|
|
1546
|
+
sound._node.bufferSource = Howler2.ctx.createBufferSource();
|
|
1547
|
+
sound._node.bufferSource.buffer = cache[self._src];
|
|
1548
|
+
if (sound._panner) {
|
|
1549
|
+
sound._node.bufferSource.connect(sound._panner);
|
|
1550
|
+
} else {
|
|
1551
|
+
sound._node.bufferSource.connect(sound._node);
|
|
1552
|
+
}
|
|
1553
|
+
sound._node.bufferSource.loop = sound._loop;
|
|
1554
|
+
if (sound._loop) {
|
|
1555
|
+
sound._node.bufferSource.loopStart = sound._start || 0;
|
|
1556
|
+
sound._node.bufferSource.loopEnd = sound._stop || 0;
|
|
1557
|
+
}
|
|
1558
|
+
sound._node.bufferSource.playbackRate.setValueAtTime(sound._rate, Howler2.ctx.currentTime);
|
|
1559
|
+
return self;
|
|
1560
|
+
},
|
|
1561
|
+
/**
|
|
1562
|
+
* Prevent memory leaks by cleaning up the buffer source after playback.
|
|
1563
|
+
* @param {Object} node Sound's audio node containing the buffer source.
|
|
1564
|
+
* @return {Howl}
|
|
1565
|
+
*/
|
|
1566
|
+
_cleanBuffer: function(node) {
|
|
1567
|
+
var self = this;
|
|
1568
|
+
var isIOS = Howler2._navigator && Howler2._navigator.vendor.indexOf("Apple") >= 0;
|
|
1569
|
+
if (!node.bufferSource) {
|
|
1570
|
+
return self;
|
|
1571
|
+
}
|
|
1572
|
+
if (Howler2._scratchBuffer && node.bufferSource) {
|
|
1573
|
+
node.bufferSource.onended = null;
|
|
1574
|
+
node.bufferSource.disconnect(0);
|
|
1575
|
+
if (isIOS) {
|
|
1576
|
+
try {
|
|
1577
|
+
node.bufferSource.buffer = Howler2._scratchBuffer;
|
|
1578
|
+
} catch (e) {
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
node.bufferSource = null;
|
|
1583
|
+
return self;
|
|
1584
|
+
},
|
|
1585
|
+
/**
|
|
1586
|
+
* Set the source to a 0-second silence to stop any downloading (except in IE).
|
|
1587
|
+
* @param {Object} node Audio node to clear.
|
|
1588
|
+
*/
|
|
1589
|
+
_clearSound: function(node) {
|
|
1590
|
+
var checkIE = /MSIE |Trident\//.test(Howler2._navigator && Howler2._navigator.userAgent);
|
|
1591
|
+
if (!checkIE) {
|
|
1592
|
+
node.src = "data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA";
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
};
|
|
1596
|
+
var Sound2 = function(howl) {
|
|
1597
|
+
this._parent = howl;
|
|
1598
|
+
this.init();
|
|
1599
|
+
};
|
|
1600
|
+
Sound2.prototype = {
|
|
1601
|
+
/**
|
|
1602
|
+
* Initialize a new Sound object.
|
|
1603
|
+
* @return {Sound}
|
|
1604
|
+
*/
|
|
1605
|
+
init: function() {
|
|
1606
|
+
var self = this;
|
|
1607
|
+
var parent = self._parent;
|
|
1608
|
+
self._muted = parent._muted;
|
|
1609
|
+
self._loop = parent._loop;
|
|
1610
|
+
self._volume = parent._volume;
|
|
1611
|
+
self._rate = parent._rate;
|
|
1612
|
+
self._seek = 0;
|
|
1613
|
+
self._paused = true;
|
|
1614
|
+
self._ended = true;
|
|
1615
|
+
self._sprite = "__default";
|
|
1616
|
+
self._id = ++Howler2._counter;
|
|
1617
|
+
parent._sounds.push(self);
|
|
1618
|
+
self.create();
|
|
1619
|
+
return self;
|
|
1620
|
+
},
|
|
1621
|
+
/**
|
|
1622
|
+
* Create and setup a new sound object, whether HTML5 Audio or Web Audio.
|
|
1623
|
+
* @return {Sound}
|
|
1624
|
+
*/
|
|
1625
|
+
create: function() {
|
|
1626
|
+
var self = this;
|
|
1627
|
+
var parent = self._parent;
|
|
1628
|
+
var volume = Howler2._muted || self._muted || self._parent._muted ? 0 : self._volume;
|
|
1629
|
+
if (parent._webAudio) {
|
|
1630
|
+
self._node = typeof Howler2.ctx.createGain === "undefined" ? Howler2.ctx.createGainNode() : Howler2.ctx.createGain();
|
|
1631
|
+
self._node.gain.setValueAtTime(volume, Howler2.ctx.currentTime);
|
|
1632
|
+
self._node.paused = true;
|
|
1633
|
+
self._node.connect(Howler2.masterGain);
|
|
1634
|
+
} else if (!Howler2.noAudio) {
|
|
1635
|
+
self._node = Howler2._obtainHtml5Audio();
|
|
1636
|
+
self._errorFn = self._errorListener.bind(self);
|
|
1637
|
+
self._node.addEventListener("error", self._errorFn, false);
|
|
1638
|
+
self._loadFn = self._loadListener.bind(self);
|
|
1639
|
+
self._node.addEventListener(Howler2._canPlayEvent, self._loadFn, false);
|
|
1640
|
+
self._endFn = self._endListener.bind(self);
|
|
1641
|
+
self._node.addEventListener("ended", self._endFn, false);
|
|
1642
|
+
self._node.src = parent._src;
|
|
1643
|
+
self._node.preload = parent._preload === true ? "auto" : parent._preload;
|
|
1644
|
+
self._node.volume = volume * Howler2.volume();
|
|
1645
|
+
self._node.load();
|
|
1646
|
+
}
|
|
1647
|
+
return self;
|
|
1648
|
+
},
|
|
1649
|
+
/**
|
|
1650
|
+
* Reset the parameters of this sound to the original state (for recycle).
|
|
1651
|
+
* @return {Sound}
|
|
1652
|
+
*/
|
|
1653
|
+
reset: function() {
|
|
1654
|
+
var self = this;
|
|
1655
|
+
var parent = self._parent;
|
|
1656
|
+
self._muted = parent._muted;
|
|
1657
|
+
self._loop = parent._loop;
|
|
1658
|
+
self._volume = parent._volume;
|
|
1659
|
+
self._rate = parent._rate;
|
|
1660
|
+
self._seek = 0;
|
|
1661
|
+
self._rateSeek = 0;
|
|
1662
|
+
self._paused = true;
|
|
1663
|
+
self._ended = true;
|
|
1664
|
+
self._sprite = "__default";
|
|
1665
|
+
self._id = ++Howler2._counter;
|
|
1666
|
+
return self;
|
|
1667
|
+
},
|
|
1668
|
+
/**
|
|
1669
|
+
* HTML5 Audio error listener callback.
|
|
1670
|
+
*/
|
|
1671
|
+
_errorListener: function() {
|
|
1672
|
+
var self = this;
|
|
1673
|
+
self._parent._emit("loaderror", self._id, self._node.error ? self._node.error.code : 0);
|
|
1674
|
+
self._node.removeEventListener("error", self._errorFn, false);
|
|
1675
|
+
},
|
|
1676
|
+
/**
|
|
1677
|
+
* HTML5 Audio canplaythrough listener callback.
|
|
1678
|
+
*/
|
|
1679
|
+
_loadListener: function() {
|
|
1680
|
+
var self = this;
|
|
1681
|
+
var parent = self._parent;
|
|
1682
|
+
parent._duration = Math.ceil(self._node.duration * 10) / 10;
|
|
1683
|
+
if (Object.keys(parent._sprite).length === 0) {
|
|
1684
|
+
parent._sprite = { __default: [0, parent._duration * 1e3] };
|
|
1685
|
+
}
|
|
1686
|
+
if (parent._state !== "loaded") {
|
|
1687
|
+
parent._state = "loaded";
|
|
1688
|
+
parent._emit("load");
|
|
1689
|
+
parent._loadQueue();
|
|
1690
|
+
}
|
|
1691
|
+
self._node.removeEventListener(Howler2._canPlayEvent, self._loadFn, false);
|
|
1692
|
+
},
|
|
1693
|
+
/**
|
|
1694
|
+
* HTML5 Audio ended listener callback.
|
|
1695
|
+
*/
|
|
1696
|
+
_endListener: function() {
|
|
1697
|
+
var self = this;
|
|
1698
|
+
var parent = self._parent;
|
|
1699
|
+
if (parent._duration === Infinity) {
|
|
1700
|
+
parent._duration = Math.ceil(self._node.duration * 10) / 10;
|
|
1701
|
+
if (parent._sprite.__default[1] === Infinity) {
|
|
1702
|
+
parent._sprite.__default[1] = parent._duration * 1e3;
|
|
1703
|
+
}
|
|
1704
|
+
parent._ended(self);
|
|
1705
|
+
}
|
|
1706
|
+
self._node.removeEventListener("ended", self._endFn, false);
|
|
1707
|
+
}
|
|
1708
|
+
};
|
|
1709
|
+
var cache = {};
|
|
1710
|
+
var loadBuffer = function(self) {
|
|
1711
|
+
var url = self._src;
|
|
1712
|
+
if (cache[url]) {
|
|
1713
|
+
self._duration = cache[url].duration;
|
|
1714
|
+
loadSound(self);
|
|
1715
|
+
return;
|
|
1716
|
+
}
|
|
1717
|
+
if (/^data:[^;]+;base64,/.test(url)) {
|
|
1718
|
+
var data = atob(url.split(",")[1]);
|
|
1719
|
+
var dataView = new Uint8Array(data.length);
|
|
1720
|
+
for (var i = 0; i < data.length; ++i) {
|
|
1721
|
+
dataView[i] = data.charCodeAt(i);
|
|
1722
|
+
}
|
|
1723
|
+
decodeAudioData(dataView.buffer, self);
|
|
1724
|
+
} else {
|
|
1725
|
+
var xhr = new XMLHttpRequest();
|
|
1726
|
+
xhr.open(self._xhr.method, url, true);
|
|
1727
|
+
xhr.withCredentials = self._xhr.withCredentials;
|
|
1728
|
+
xhr.responseType = "arraybuffer";
|
|
1729
|
+
if (self._xhr.headers) {
|
|
1730
|
+
Object.keys(self._xhr.headers).forEach(function(key) {
|
|
1731
|
+
xhr.setRequestHeader(key, self._xhr.headers[key]);
|
|
1732
|
+
});
|
|
1733
|
+
}
|
|
1734
|
+
xhr.onload = function() {
|
|
1735
|
+
var code = (xhr.status + "")[0];
|
|
1736
|
+
if (code !== "0" && code !== "2" && code !== "3") {
|
|
1737
|
+
self._emit("loaderror", null, "Failed loading audio file with status: " + xhr.status + ".");
|
|
1738
|
+
return;
|
|
1739
|
+
}
|
|
1740
|
+
decodeAudioData(xhr.response, self);
|
|
1741
|
+
};
|
|
1742
|
+
xhr.onerror = function() {
|
|
1743
|
+
if (self._webAudio) {
|
|
1744
|
+
self._html5 = true;
|
|
1745
|
+
self._webAudio = false;
|
|
1746
|
+
self._sounds = [];
|
|
1747
|
+
delete cache[url];
|
|
1748
|
+
self.load();
|
|
1749
|
+
}
|
|
1750
|
+
};
|
|
1751
|
+
safeXhrSend(xhr);
|
|
1752
|
+
}
|
|
1753
|
+
};
|
|
1754
|
+
var safeXhrSend = function(xhr) {
|
|
1755
|
+
try {
|
|
1756
|
+
xhr.send();
|
|
1757
|
+
} catch (e) {
|
|
1758
|
+
xhr.onerror();
|
|
1759
|
+
}
|
|
1760
|
+
};
|
|
1761
|
+
var decodeAudioData = function(arraybuffer, self) {
|
|
1762
|
+
var error = function() {
|
|
1763
|
+
self._emit("loaderror", null, "Decoding audio data failed.");
|
|
1764
|
+
};
|
|
1765
|
+
var success = function(buffer) {
|
|
1766
|
+
if (buffer && self._sounds.length > 0) {
|
|
1767
|
+
cache[self._src] = buffer;
|
|
1768
|
+
loadSound(self, buffer);
|
|
1769
|
+
} else {
|
|
1770
|
+
error();
|
|
1771
|
+
}
|
|
1772
|
+
};
|
|
1773
|
+
if (typeof Promise !== "undefined" && Howler2.ctx.decodeAudioData.length === 1) {
|
|
1774
|
+
Howler2.ctx.decodeAudioData(arraybuffer).then(success).catch(error);
|
|
1775
|
+
} else {
|
|
1776
|
+
Howler2.ctx.decodeAudioData(arraybuffer, success, error);
|
|
1777
|
+
}
|
|
1778
|
+
};
|
|
1779
|
+
var loadSound = function(self, buffer) {
|
|
1780
|
+
if (buffer && !self._duration) {
|
|
1781
|
+
self._duration = buffer.duration;
|
|
1782
|
+
}
|
|
1783
|
+
if (Object.keys(self._sprite).length === 0) {
|
|
1784
|
+
self._sprite = { __default: [0, self._duration * 1e3] };
|
|
1785
|
+
}
|
|
1786
|
+
if (self._state !== "loaded") {
|
|
1787
|
+
self._state = "loaded";
|
|
1788
|
+
self._emit("load");
|
|
1789
|
+
self._loadQueue();
|
|
1790
|
+
}
|
|
1791
|
+
};
|
|
1792
|
+
var setupAudioContext = function() {
|
|
1793
|
+
if (!Howler2.usingWebAudio) {
|
|
1794
|
+
return;
|
|
1795
|
+
}
|
|
1796
|
+
try {
|
|
1797
|
+
if (typeof AudioContext !== "undefined") {
|
|
1798
|
+
Howler2.ctx = new AudioContext();
|
|
1799
|
+
} else if (typeof webkitAudioContext !== "undefined") {
|
|
1800
|
+
Howler2.ctx = new webkitAudioContext();
|
|
1801
|
+
} else {
|
|
1802
|
+
Howler2.usingWebAudio = false;
|
|
1803
|
+
}
|
|
1804
|
+
} catch (e) {
|
|
1805
|
+
Howler2.usingWebAudio = false;
|
|
1806
|
+
}
|
|
1807
|
+
if (!Howler2.ctx) {
|
|
1808
|
+
Howler2.usingWebAudio = false;
|
|
1809
|
+
}
|
|
1810
|
+
var iOS = /iP(hone|od|ad)/.test(Howler2._navigator && Howler2._navigator.platform);
|
|
1811
|
+
var appVersion = Howler2._navigator && Howler2._navigator.appVersion.match(/OS (\d+)_(\d+)_?(\d+)?/);
|
|
1812
|
+
var version = appVersion ? parseInt(appVersion[1], 10) : null;
|
|
1813
|
+
if (iOS && version && version < 9) {
|
|
1814
|
+
var safari = /safari/.test(Howler2._navigator && Howler2._navigator.userAgent.toLowerCase());
|
|
1815
|
+
if (Howler2._navigator && !safari) {
|
|
1816
|
+
Howler2.usingWebAudio = false;
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
if (Howler2.usingWebAudio) {
|
|
1820
|
+
Howler2.masterGain = typeof Howler2.ctx.createGain === "undefined" ? Howler2.ctx.createGainNode() : Howler2.ctx.createGain();
|
|
1821
|
+
Howler2.masterGain.gain.setValueAtTime(Howler2._muted ? 0 : Howler2._volume, Howler2.ctx.currentTime);
|
|
1822
|
+
Howler2.masterGain.connect(Howler2.ctx.destination);
|
|
1823
|
+
}
|
|
1824
|
+
Howler2._setup();
|
|
1825
|
+
};
|
|
1826
|
+
if (typeof define === "function" && define.amd) {
|
|
1827
|
+
define([], function() {
|
|
1828
|
+
return {
|
|
1829
|
+
Howler: Howler2,
|
|
1830
|
+
Howl: Howl3
|
|
1831
|
+
};
|
|
1832
|
+
});
|
|
1833
|
+
}
|
|
1834
|
+
if (typeof exports !== "undefined") {
|
|
1835
|
+
exports.Howler = Howler2;
|
|
1836
|
+
exports.Howl = Howl3;
|
|
1837
|
+
}
|
|
1838
|
+
if (typeof global !== "undefined") {
|
|
1839
|
+
global.HowlerGlobal = HowlerGlobal2;
|
|
1840
|
+
global.Howler = Howler2;
|
|
1841
|
+
global.Howl = Howl3;
|
|
1842
|
+
global.Sound = Sound2;
|
|
1843
|
+
} else if (typeof window !== "undefined") {
|
|
1844
|
+
window.HowlerGlobal = HowlerGlobal2;
|
|
1845
|
+
window.Howler = Howler2;
|
|
1846
|
+
window.Howl = Howl3;
|
|
1847
|
+
window.Sound = Sound2;
|
|
1848
|
+
}
|
|
1849
|
+
})();
|
|
1850
|
+
(function() {
|
|
1851
|
+
"use strict";
|
|
1852
|
+
HowlerGlobal.prototype._pos = [0, 0, 0];
|
|
1853
|
+
HowlerGlobal.prototype._orientation = [0, 0, -1, 0, 1, 0];
|
|
1854
|
+
HowlerGlobal.prototype.stereo = function(pan) {
|
|
1855
|
+
var self = this;
|
|
1856
|
+
if (!self.ctx || !self.ctx.listener) {
|
|
1857
|
+
return self;
|
|
1858
|
+
}
|
|
1859
|
+
for (var i = self._howls.length - 1; i >= 0; i--) {
|
|
1860
|
+
self._howls[i].stereo(pan);
|
|
1861
|
+
}
|
|
1862
|
+
return self;
|
|
1863
|
+
};
|
|
1864
|
+
HowlerGlobal.prototype.pos = function(x, y, z) {
|
|
1865
|
+
var self = this;
|
|
1866
|
+
if (!self.ctx || !self.ctx.listener) {
|
|
1867
|
+
return self;
|
|
1868
|
+
}
|
|
1869
|
+
y = typeof y !== "number" ? self._pos[1] : y;
|
|
1870
|
+
z = typeof z !== "number" ? self._pos[2] : z;
|
|
1871
|
+
if (typeof x === "number") {
|
|
1872
|
+
self._pos = [x, y, z];
|
|
1873
|
+
if (typeof self.ctx.listener.positionX !== "undefined") {
|
|
1874
|
+
self.ctx.listener.positionX.setTargetAtTime(self._pos[0], Howler.ctx.currentTime, 0.1);
|
|
1875
|
+
self.ctx.listener.positionY.setTargetAtTime(self._pos[1], Howler.ctx.currentTime, 0.1);
|
|
1876
|
+
self.ctx.listener.positionZ.setTargetAtTime(self._pos[2], Howler.ctx.currentTime, 0.1);
|
|
1877
|
+
} else {
|
|
1878
|
+
self.ctx.listener.setPosition(self._pos[0], self._pos[1], self._pos[2]);
|
|
1879
|
+
}
|
|
1880
|
+
} else {
|
|
1881
|
+
return self._pos;
|
|
1882
|
+
}
|
|
1883
|
+
return self;
|
|
1884
|
+
};
|
|
1885
|
+
HowlerGlobal.prototype.orientation = function(x, y, z, xUp, yUp, zUp) {
|
|
1886
|
+
var self = this;
|
|
1887
|
+
if (!self.ctx || !self.ctx.listener) {
|
|
1888
|
+
return self;
|
|
1889
|
+
}
|
|
1890
|
+
var or = self._orientation;
|
|
1891
|
+
y = typeof y !== "number" ? or[1] : y;
|
|
1892
|
+
z = typeof z !== "number" ? or[2] : z;
|
|
1893
|
+
xUp = typeof xUp !== "number" ? or[3] : xUp;
|
|
1894
|
+
yUp = typeof yUp !== "number" ? or[4] : yUp;
|
|
1895
|
+
zUp = typeof zUp !== "number" ? or[5] : zUp;
|
|
1896
|
+
if (typeof x === "number") {
|
|
1897
|
+
self._orientation = [x, y, z, xUp, yUp, zUp];
|
|
1898
|
+
if (typeof self.ctx.listener.forwardX !== "undefined") {
|
|
1899
|
+
self.ctx.listener.forwardX.setTargetAtTime(x, Howler.ctx.currentTime, 0.1);
|
|
1900
|
+
self.ctx.listener.forwardY.setTargetAtTime(y, Howler.ctx.currentTime, 0.1);
|
|
1901
|
+
self.ctx.listener.forwardZ.setTargetAtTime(z, Howler.ctx.currentTime, 0.1);
|
|
1902
|
+
self.ctx.listener.upX.setTargetAtTime(xUp, Howler.ctx.currentTime, 0.1);
|
|
1903
|
+
self.ctx.listener.upY.setTargetAtTime(yUp, Howler.ctx.currentTime, 0.1);
|
|
1904
|
+
self.ctx.listener.upZ.setTargetAtTime(zUp, Howler.ctx.currentTime, 0.1);
|
|
1905
|
+
} else {
|
|
1906
|
+
self.ctx.listener.setOrientation(x, y, z, xUp, yUp, zUp);
|
|
1907
|
+
}
|
|
1908
|
+
} else {
|
|
1909
|
+
return or;
|
|
1910
|
+
}
|
|
1911
|
+
return self;
|
|
1912
|
+
};
|
|
1913
|
+
Howl.prototype.init = /* @__PURE__ */ (function(_super) {
|
|
1914
|
+
return function(o) {
|
|
1915
|
+
var self = this;
|
|
1916
|
+
self._orientation = o.orientation || [1, 0, 0];
|
|
1917
|
+
self._stereo = o.stereo || null;
|
|
1918
|
+
self._pos = o.pos || null;
|
|
1919
|
+
self._pannerAttr = {
|
|
1920
|
+
coneInnerAngle: typeof o.coneInnerAngle !== "undefined" ? o.coneInnerAngle : 360,
|
|
1921
|
+
coneOuterAngle: typeof o.coneOuterAngle !== "undefined" ? o.coneOuterAngle : 360,
|
|
1922
|
+
coneOuterGain: typeof o.coneOuterGain !== "undefined" ? o.coneOuterGain : 0,
|
|
1923
|
+
distanceModel: typeof o.distanceModel !== "undefined" ? o.distanceModel : "inverse",
|
|
1924
|
+
maxDistance: typeof o.maxDistance !== "undefined" ? o.maxDistance : 1e4,
|
|
1925
|
+
panningModel: typeof o.panningModel !== "undefined" ? o.panningModel : "HRTF",
|
|
1926
|
+
refDistance: typeof o.refDistance !== "undefined" ? o.refDistance : 1,
|
|
1927
|
+
rolloffFactor: typeof o.rolloffFactor !== "undefined" ? o.rolloffFactor : 1
|
|
1928
|
+
};
|
|
1929
|
+
self._onstereo = o.onstereo ? [{ fn: o.onstereo }] : [];
|
|
1930
|
+
self._onpos = o.onpos ? [{ fn: o.onpos }] : [];
|
|
1931
|
+
self._onorientation = o.onorientation ? [{ fn: o.onorientation }] : [];
|
|
1932
|
+
return _super.call(this, o);
|
|
1933
|
+
};
|
|
1934
|
+
})(Howl.prototype.init);
|
|
1935
|
+
Howl.prototype.stereo = function(pan, id) {
|
|
1936
|
+
var self = this;
|
|
1937
|
+
if (!self._webAudio) {
|
|
1938
|
+
return self;
|
|
1939
|
+
}
|
|
1940
|
+
if (self._state !== "loaded") {
|
|
1941
|
+
self._queue.push({
|
|
1942
|
+
event: "stereo",
|
|
1943
|
+
action: function() {
|
|
1944
|
+
self.stereo(pan, id);
|
|
1945
|
+
}
|
|
1946
|
+
});
|
|
1947
|
+
return self;
|
|
1948
|
+
}
|
|
1949
|
+
var pannerType = typeof Howler.ctx.createStereoPanner === "undefined" ? "spatial" : "stereo";
|
|
1950
|
+
if (typeof id === "undefined") {
|
|
1951
|
+
if (typeof pan === "number") {
|
|
1952
|
+
self._stereo = pan;
|
|
1953
|
+
self._pos = [pan, 0, 0];
|
|
1954
|
+
} else {
|
|
1955
|
+
return self._stereo;
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
var ids = self._getSoundIds(id);
|
|
1959
|
+
for (var i = 0; i < ids.length; i++) {
|
|
1960
|
+
var sound = self._soundById(ids[i]);
|
|
1961
|
+
if (sound) {
|
|
1962
|
+
if (typeof pan === "number") {
|
|
1963
|
+
sound._stereo = pan;
|
|
1964
|
+
sound._pos = [pan, 0, 0];
|
|
1965
|
+
if (sound._node) {
|
|
1966
|
+
sound._pannerAttr.panningModel = "equalpower";
|
|
1967
|
+
if (!sound._panner || !sound._panner.pan) {
|
|
1968
|
+
setupPanner(sound, pannerType);
|
|
1969
|
+
}
|
|
1970
|
+
if (pannerType === "spatial") {
|
|
1971
|
+
if (typeof sound._panner.positionX !== "undefined") {
|
|
1972
|
+
sound._panner.positionX.setValueAtTime(pan, Howler.ctx.currentTime);
|
|
1973
|
+
sound._panner.positionY.setValueAtTime(0, Howler.ctx.currentTime);
|
|
1974
|
+
sound._panner.positionZ.setValueAtTime(0, Howler.ctx.currentTime);
|
|
1975
|
+
} else {
|
|
1976
|
+
sound._panner.setPosition(pan, 0, 0);
|
|
1977
|
+
}
|
|
1978
|
+
} else {
|
|
1979
|
+
sound._panner.pan.setValueAtTime(pan, Howler.ctx.currentTime);
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
self._emit("stereo", sound._id);
|
|
1983
|
+
} else {
|
|
1984
|
+
return sound._stereo;
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
return self;
|
|
1989
|
+
};
|
|
1990
|
+
Howl.prototype.pos = function(x, y, z, id) {
|
|
1991
|
+
var self = this;
|
|
1992
|
+
if (!self._webAudio) {
|
|
1993
|
+
return self;
|
|
1994
|
+
}
|
|
1995
|
+
if (self._state !== "loaded") {
|
|
1996
|
+
self._queue.push({
|
|
1997
|
+
event: "pos",
|
|
1998
|
+
action: function() {
|
|
1999
|
+
self.pos(x, y, z, id);
|
|
2000
|
+
}
|
|
2001
|
+
});
|
|
2002
|
+
return self;
|
|
2003
|
+
}
|
|
2004
|
+
y = typeof y !== "number" ? 0 : y;
|
|
2005
|
+
z = typeof z !== "number" ? -0.5 : z;
|
|
2006
|
+
if (typeof id === "undefined") {
|
|
2007
|
+
if (typeof x === "number") {
|
|
2008
|
+
self._pos = [x, y, z];
|
|
2009
|
+
} else {
|
|
2010
|
+
return self._pos;
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
var ids = self._getSoundIds(id);
|
|
2014
|
+
for (var i = 0; i < ids.length; i++) {
|
|
2015
|
+
var sound = self._soundById(ids[i]);
|
|
2016
|
+
if (sound) {
|
|
2017
|
+
if (typeof x === "number") {
|
|
2018
|
+
sound._pos = [x, y, z];
|
|
2019
|
+
if (sound._node) {
|
|
2020
|
+
if (!sound._panner || sound._panner.pan) {
|
|
2021
|
+
setupPanner(sound, "spatial");
|
|
2022
|
+
}
|
|
2023
|
+
if (typeof sound._panner.positionX !== "undefined") {
|
|
2024
|
+
sound._panner.positionX.setValueAtTime(x, Howler.ctx.currentTime);
|
|
2025
|
+
sound._panner.positionY.setValueAtTime(y, Howler.ctx.currentTime);
|
|
2026
|
+
sound._panner.positionZ.setValueAtTime(z, Howler.ctx.currentTime);
|
|
2027
|
+
} else {
|
|
2028
|
+
sound._panner.setPosition(x, y, z);
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
self._emit("pos", sound._id);
|
|
2032
|
+
} else {
|
|
2033
|
+
return sound._pos;
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
return self;
|
|
2038
|
+
};
|
|
2039
|
+
Howl.prototype.orientation = function(x, y, z, id) {
|
|
2040
|
+
var self = this;
|
|
2041
|
+
if (!self._webAudio) {
|
|
2042
|
+
return self;
|
|
2043
|
+
}
|
|
2044
|
+
if (self._state !== "loaded") {
|
|
2045
|
+
self._queue.push({
|
|
2046
|
+
event: "orientation",
|
|
2047
|
+
action: function() {
|
|
2048
|
+
self.orientation(x, y, z, id);
|
|
2049
|
+
}
|
|
2050
|
+
});
|
|
2051
|
+
return self;
|
|
2052
|
+
}
|
|
2053
|
+
y = typeof y !== "number" ? self._orientation[1] : y;
|
|
2054
|
+
z = typeof z !== "number" ? self._orientation[2] : z;
|
|
2055
|
+
if (typeof id === "undefined") {
|
|
2056
|
+
if (typeof x === "number") {
|
|
2057
|
+
self._orientation = [x, y, z];
|
|
2058
|
+
} else {
|
|
2059
|
+
return self._orientation;
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
var ids = self._getSoundIds(id);
|
|
2063
|
+
for (var i = 0; i < ids.length; i++) {
|
|
2064
|
+
var sound = self._soundById(ids[i]);
|
|
2065
|
+
if (sound) {
|
|
2066
|
+
if (typeof x === "number") {
|
|
2067
|
+
sound._orientation = [x, y, z];
|
|
2068
|
+
if (sound._node) {
|
|
2069
|
+
if (!sound._panner) {
|
|
2070
|
+
if (!sound._pos) {
|
|
2071
|
+
sound._pos = self._pos || [0, 0, -0.5];
|
|
2072
|
+
}
|
|
2073
|
+
setupPanner(sound, "spatial");
|
|
2074
|
+
}
|
|
2075
|
+
if (typeof sound._panner.orientationX !== "undefined") {
|
|
2076
|
+
sound._panner.orientationX.setValueAtTime(x, Howler.ctx.currentTime);
|
|
2077
|
+
sound._panner.orientationY.setValueAtTime(y, Howler.ctx.currentTime);
|
|
2078
|
+
sound._panner.orientationZ.setValueAtTime(z, Howler.ctx.currentTime);
|
|
2079
|
+
} else {
|
|
2080
|
+
sound._panner.setOrientation(x, y, z);
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
self._emit("orientation", sound._id);
|
|
2084
|
+
} else {
|
|
2085
|
+
return sound._orientation;
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
return self;
|
|
2090
|
+
};
|
|
2091
|
+
Howl.prototype.pannerAttr = function() {
|
|
2092
|
+
var self = this;
|
|
2093
|
+
var args = arguments;
|
|
2094
|
+
var o, id, sound;
|
|
2095
|
+
if (!self._webAudio) {
|
|
2096
|
+
return self;
|
|
2097
|
+
}
|
|
2098
|
+
if (args.length === 0) {
|
|
2099
|
+
return self._pannerAttr;
|
|
2100
|
+
} else if (args.length === 1) {
|
|
2101
|
+
if (typeof args[0] === "object") {
|
|
2102
|
+
o = args[0];
|
|
2103
|
+
if (typeof id === "undefined") {
|
|
2104
|
+
if (!o.pannerAttr) {
|
|
2105
|
+
o.pannerAttr = {
|
|
2106
|
+
coneInnerAngle: o.coneInnerAngle,
|
|
2107
|
+
coneOuterAngle: o.coneOuterAngle,
|
|
2108
|
+
coneOuterGain: o.coneOuterGain,
|
|
2109
|
+
distanceModel: o.distanceModel,
|
|
2110
|
+
maxDistance: o.maxDistance,
|
|
2111
|
+
refDistance: o.refDistance,
|
|
2112
|
+
rolloffFactor: o.rolloffFactor,
|
|
2113
|
+
panningModel: o.panningModel
|
|
2114
|
+
};
|
|
2115
|
+
}
|
|
2116
|
+
self._pannerAttr = {
|
|
2117
|
+
coneInnerAngle: typeof o.pannerAttr.coneInnerAngle !== "undefined" ? o.pannerAttr.coneInnerAngle : self._coneInnerAngle,
|
|
2118
|
+
coneOuterAngle: typeof o.pannerAttr.coneOuterAngle !== "undefined" ? o.pannerAttr.coneOuterAngle : self._coneOuterAngle,
|
|
2119
|
+
coneOuterGain: typeof o.pannerAttr.coneOuterGain !== "undefined" ? o.pannerAttr.coneOuterGain : self._coneOuterGain,
|
|
2120
|
+
distanceModel: typeof o.pannerAttr.distanceModel !== "undefined" ? o.pannerAttr.distanceModel : self._distanceModel,
|
|
2121
|
+
maxDistance: typeof o.pannerAttr.maxDistance !== "undefined" ? o.pannerAttr.maxDistance : self._maxDistance,
|
|
2122
|
+
refDistance: typeof o.pannerAttr.refDistance !== "undefined" ? o.pannerAttr.refDistance : self._refDistance,
|
|
2123
|
+
rolloffFactor: typeof o.pannerAttr.rolloffFactor !== "undefined" ? o.pannerAttr.rolloffFactor : self._rolloffFactor,
|
|
2124
|
+
panningModel: typeof o.pannerAttr.panningModel !== "undefined" ? o.pannerAttr.panningModel : self._panningModel
|
|
2125
|
+
};
|
|
2126
|
+
}
|
|
2127
|
+
} else {
|
|
2128
|
+
sound = self._soundById(parseInt(args[0], 10));
|
|
2129
|
+
return sound ? sound._pannerAttr : self._pannerAttr;
|
|
2130
|
+
}
|
|
2131
|
+
} else if (args.length === 2) {
|
|
2132
|
+
o = args[0];
|
|
2133
|
+
id = parseInt(args[1], 10);
|
|
2134
|
+
}
|
|
2135
|
+
var ids = self._getSoundIds(id);
|
|
2136
|
+
for (var i = 0; i < ids.length; i++) {
|
|
2137
|
+
sound = self._soundById(ids[i]);
|
|
2138
|
+
if (sound) {
|
|
2139
|
+
var pa = sound._pannerAttr;
|
|
2140
|
+
pa = {
|
|
2141
|
+
coneInnerAngle: typeof o.coneInnerAngle !== "undefined" ? o.coneInnerAngle : pa.coneInnerAngle,
|
|
2142
|
+
coneOuterAngle: typeof o.coneOuterAngle !== "undefined" ? o.coneOuterAngle : pa.coneOuterAngle,
|
|
2143
|
+
coneOuterGain: typeof o.coneOuterGain !== "undefined" ? o.coneOuterGain : pa.coneOuterGain,
|
|
2144
|
+
distanceModel: typeof o.distanceModel !== "undefined" ? o.distanceModel : pa.distanceModel,
|
|
2145
|
+
maxDistance: typeof o.maxDistance !== "undefined" ? o.maxDistance : pa.maxDistance,
|
|
2146
|
+
refDistance: typeof o.refDistance !== "undefined" ? o.refDistance : pa.refDistance,
|
|
2147
|
+
rolloffFactor: typeof o.rolloffFactor !== "undefined" ? o.rolloffFactor : pa.rolloffFactor,
|
|
2148
|
+
panningModel: typeof o.panningModel !== "undefined" ? o.panningModel : pa.panningModel
|
|
2149
|
+
};
|
|
2150
|
+
var panner = sound._panner;
|
|
2151
|
+
if (!panner) {
|
|
2152
|
+
if (!sound._pos) {
|
|
2153
|
+
sound._pos = self._pos || [0, 0, -0.5];
|
|
2154
|
+
}
|
|
2155
|
+
setupPanner(sound, "spatial");
|
|
2156
|
+
panner = sound._panner;
|
|
2157
|
+
}
|
|
2158
|
+
panner.coneInnerAngle = pa.coneInnerAngle;
|
|
2159
|
+
panner.coneOuterAngle = pa.coneOuterAngle;
|
|
2160
|
+
panner.coneOuterGain = pa.coneOuterGain;
|
|
2161
|
+
panner.distanceModel = pa.distanceModel;
|
|
2162
|
+
panner.maxDistance = pa.maxDistance;
|
|
2163
|
+
panner.refDistance = pa.refDistance;
|
|
2164
|
+
panner.rolloffFactor = pa.rolloffFactor;
|
|
2165
|
+
panner.panningModel = pa.panningModel;
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
return self;
|
|
2169
|
+
};
|
|
2170
|
+
Sound.prototype.init = /* @__PURE__ */ (function(_super) {
|
|
2171
|
+
return function() {
|
|
2172
|
+
var self = this;
|
|
2173
|
+
var parent = self._parent;
|
|
2174
|
+
self._orientation = parent._orientation;
|
|
2175
|
+
self._stereo = parent._stereo;
|
|
2176
|
+
self._pos = parent._pos;
|
|
2177
|
+
self._pannerAttr = parent._pannerAttr;
|
|
2178
|
+
_super.call(this);
|
|
2179
|
+
if (self._stereo) {
|
|
2180
|
+
parent.stereo(self._stereo);
|
|
2181
|
+
} else if (self._pos) {
|
|
2182
|
+
parent.pos(self._pos[0], self._pos[1], self._pos[2], self._id);
|
|
2183
|
+
}
|
|
2184
|
+
};
|
|
2185
|
+
})(Sound.prototype.init);
|
|
2186
|
+
Sound.prototype.reset = /* @__PURE__ */ (function(_super) {
|
|
2187
|
+
return function() {
|
|
2188
|
+
var self = this;
|
|
2189
|
+
var parent = self._parent;
|
|
2190
|
+
self._orientation = parent._orientation;
|
|
2191
|
+
self._stereo = parent._stereo;
|
|
2192
|
+
self._pos = parent._pos;
|
|
2193
|
+
self._pannerAttr = parent._pannerAttr;
|
|
2194
|
+
if (self._stereo) {
|
|
2195
|
+
parent.stereo(self._stereo);
|
|
2196
|
+
} else if (self._pos) {
|
|
2197
|
+
parent.pos(self._pos[0], self._pos[1], self._pos[2], self._id);
|
|
2198
|
+
} else if (self._panner) {
|
|
2199
|
+
self._panner.disconnect(0);
|
|
2200
|
+
self._panner = void 0;
|
|
2201
|
+
parent._refreshBuffer(self);
|
|
2202
|
+
}
|
|
2203
|
+
return _super.call(this);
|
|
2204
|
+
};
|
|
2205
|
+
})(Sound.prototype.reset);
|
|
2206
|
+
var setupPanner = function(sound, type) {
|
|
2207
|
+
type = type || "spatial";
|
|
2208
|
+
if (type === "spatial") {
|
|
2209
|
+
sound._panner = Howler.ctx.createPanner();
|
|
2210
|
+
sound._panner.coneInnerAngle = sound._pannerAttr.coneInnerAngle;
|
|
2211
|
+
sound._panner.coneOuterAngle = sound._pannerAttr.coneOuterAngle;
|
|
2212
|
+
sound._panner.coneOuterGain = sound._pannerAttr.coneOuterGain;
|
|
2213
|
+
sound._panner.distanceModel = sound._pannerAttr.distanceModel;
|
|
2214
|
+
sound._panner.maxDistance = sound._pannerAttr.maxDistance;
|
|
2215
|
+
sound._panner.refDistance = sound._pannerAttr.refDistance;
|
|
2216
|
+
sound._panner.rolloffFactor = sound._pannerAttr.rolloffFactor;
|
|
2217
|
+
sound._panner.panningModel = sound._pannerAttr.panningModel;
|
|
2218
|
+
if (typeof sound._panner.positionX !== "undefined") {
|
|
2219
|
+
sound._panner.positionX.setValueAtTime(sound._pos[0], Howler.ctx.currentTime);
|
|
2220
|
+
sound._panner.positionY.setValueAtTime(sound._pos[1], Howler.ctx.currentTime);
|
|
2221
|
+
sound._panner.positionZ.setValueAtTime(sound._pos[2], Howler.ctx.currentTime);
|
|
2222
|
+
} else {
|
|
2223
|
+
sound._panner.setPosition(sound._pos[0], sound._pos[1], sound._pos[2]);
|
|
2224
|
+
}
|
|
2225
|
+
if (typeof sound._panner.orientationX !== "undefined") {
|
|
2226
|
+
sound._panner.orientationX.setValueAtTime(sound._orientation[0], Howler.ctx.currentTime);
|
|
2227
|
+
sound._panner.orientationY.setValueAtTime(sound._orientation[1], Howler.ctx.currentTime);
|
|
2228
|
+
sound._panner.orientationZ.setValueAtTime(sound._orientation[2], Howler.ctx.currentTime);
|
|
2229
|
+
} else {
|
|
2230
|
+
sound._panner.setOrientation(sound._orientation[0], sound._orientation[1], sound._orientation[2]);
|
|
2231
|
+
}
|
|
2232
|
+
} else {
|
|
2233
|
+
sound._panner = Howler.ctx.createStereoPanner();
|
|
2234
|
+
sound._panner.pan.setValueAtTime(sound._stereo, Howler.ctx.currentTime);
|
|
2235
|
+
}
|
|
2236
|
+
sound._panner.connect(sound._node);
|
|
2237
|
+
if (!sound._paused) {
|
|
2238
|
+
sound._parent.pause(sound._id, true).play(sound._id, true);
|
|
2239
|
+
}
|
|
2240
|
+
};
|
|
2241
|
+
})();
|
|
2242
|
+
}
|
|
2243
|
+
});
|
|
2244
|
+
|
|
1
2245
|
// src/plugin.ts
|
|
2
2246
|
import { tool } from "@opencode-ai/plugin";
|
|
3
2247
|
|
|
4
2248
|
// src/core/speaker.ts
|
|
5
2249
|
import { EventEmitter as EventEmitter2 } from "events";
|
|
6
2250
|
|
|
7
|
-
// src/core/types.ts
|
|
8
|
-
var TTSError = class extends Error {
|
|
9
|
-
constructor(message, code, provider, details) {
|
|
10
|
-
super(message);
|
|
11
|
-
this.name = "TTSError";
|
|
12
|
-
this.code = code;
|
|
13
|
-
this.provider = provider;
|
|
14
|
-
this.details = details;
|
|
2251
|
+
// src/core/types.ts
|
|
2252
|
+
var TTSError = class extends Error {
|
|
2253
|
+
constructor(message, code, provider, details) {
|
|
2254
|
+
super(message);
|
|
2255
|
+
this.name = "TTSError";
|
|
2256
|
+
this.code = code;
|
|
2257
|
+
this.provider = provider;
|
|
2258
|
+
this.details = details;
|
|
2259
|
+
}
|
|
2260
|
+
code;
|
|
2261
|
+
provider;
|
|
2262
|
+
details;
|
|
2263
|
+
};
|
|
2264
|
+
|
|
2265
|
+
// src/providers/base.ts
|
|
2266
|
+
var providers = /* @__PURE__ */ new Map();
|
|
2267
|
+
function registerProvider(name, provider) {
|
|
2268
|
+
if (providers.has(name)) {
|
|
2269
|
+
throw new TTSError(
|
|
2270
|
+
`Provider "${name}" is already registered`,
|
|
2271
|
+
"UNKNOWN" /* UNKNOWN */,
|
|
2272
|
+
"system"
|
|
2273
|
+
);
|
|
2274
|
+
}
|
|
2275
|
+
providers.set(name, provider);
|
|
2276
|
+
}
|
|
2277
|
+
function getProvider(name) {
|
|
2278
|
+
const provider = providers.get(name);
|
|
2279
|
+
if (!provider) {
|
|
2280
|
+
throw new TTSError(
|
|
2281
|
+
`TTS Provider "${name}" not found`,
|
|
2282
|
+
"UNKNOWN" /* UNKNOWN */,
|
|
2283
|
+
"system"
|
|
2284
|
+
);
|
|
2285
|
+
}
|
|
2286
|
+
return provider;
|
|
2287
|
+
}
|
|
2288
|
+
function listProviders() {
|
|
2289
|
+
return Array.from(providers.keys());
|
|
2290
|
+
}
|
|
2291
|
+
function hasProvider(name) {
|
|
2292
|
+
return providers.has(name);
|
|
2293
|
+
}
|
|
2294
|
+
var BaseTTSProvider = class {
|
|
2295
|
+
apiKey;
|
|
2296
|
+
defaultVoice;
|
|
2297
|
+
defaultModel = "stream";
|
|
2298
|
+
async initialize() {
|
|
2299
|
+
}
|
|
2300
|
+
async destroy() {
|
|
2301
|
+
}
|
|
2302
|
+
/**
|
|
2303
|
+
* 通用 speak 实现,处理通用逻辑
|
|
2304
|
+
*/
|
|
2305
|
+
async speak(text, options) {
|
|
2306
|
+
if (!text || text.trim().length === 0) {
|
|
2307
|
+
throw new TTSError(
|
|
2308
|
+
"Text cannot be empty",
|
|
2309
|
+
"INVALID_PARAMS" /* INVALID_PARAMS */,
|
|
2310
|
+
this.name
|
|
2311
|
+
);
|
|
2312
|
+
}
|
|
2313
|
+
const voice = options?.voice || this.defaultVoice;
|
|
2314
|
+
const model = options?.model || this.defaultModel;
|
|
2315
|
+
return this.doSpeak(text, voice, model, options);
|
|
2316
|
+
}
|
|
2317
|
+
pause() {
|
|
2318
|
+
throw new TTSError(
|
|
2319
|
+
"Pause is not supported by this provider",
|
|
2320
|
+
"UNKNOWN" /* UNKNOWN */,
|
|
2321
|
+
this.name
|
|
2322
|
+
);
|
|
2323
|
+
}
|
|
2324
|
+
resume() {
|
|
2325
|
+
throw new TTSError(
|
|
2326
|
+
"Resume is not supported by this provider",
|
|
2327
|
+
"UNKNOWN" /* UNKNOWN */,
|
|
2328
|
+
this.name
|
|
2329
|
+
);
|
|
2330
|
+
}
|
|
2331
|
+
stop() {
|
|
2332
|
+
return Promise.resolve();
|
|
2333
|
+
}
|
|
2334
|
+
async listVoices() {
|
|
2335
|
+
return [];
|
|
2336
|
+
}
|
|
2337
|
+
getCapabilities() {
|
|
2338
|
+
return this.capabilities;
|
|
2339
|
+
}
|
|
2340
|
+
/**
|
|
2341
|
+
* 验证 API Key
|
|
2342
|
+
*/
|
|
2343
|
+
validateApiKey() {
|
|
2344
|
+
if (!this.apiKey) {
|
|
2345
|
+
throw new TTSError(
|
|
2346
|
+
`API key is required for provider "${this.name}"`,
|
|
2347
|
+
"AUTH" /* AUTH */,
|
|
2348
|
+
this.name
|
|
2349
|
+
);
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
};
|
|
2353
|
+
|
|
2354
|
+
// src/core/player.ts
|
|
2355
|
+
import { EventEmitter } from "events";
|
|
2356
|
+
import fs from "fs";
|
|
2357
|
+
import { createWriteStream } from "fs";
|
|
2358
|
+
import { tmpdir as tmpdir4 } from "os";
|
|
2359
|
+
import { join as join4 } from "path";
|
|
2360
|
+
|
|
2361
|
+
// src/core/backends/naudiodon-backend.ts
|
|
2362
|
+
var UnsupportedError = class extends Error {
|
|
2363
|
+
constructor(message) {
|
|
2364
|
+
super(message);
|
|
2365
|
+
this.name = "UnsupportedError";
|
|
2366
|
+
}
|
|
2367
|
+
};
|
|
2368
|
+
var NaudiodonBackend = class {
|
|
2369
|
+
name = "naudiodon";
|
|
2370
|
+
supportsStreaming = true;
|
|
2371
|
+
audioOutput;
|
|
2372
|
+
events;
|
|
2373
|
+
_started = false;
|
|
2374
|
+
_paused = false;
|
|
2375
|
+
_stopped = false;
|
|
2376
|
+
sampleRate;
|
|
2377
|
+
channels;
|
|
2378
|
+
volume = 1;
|
|
2379
|
+
bytesWritten = 0;
|
|
2380
|
+
constructor(options = {}) {
|
|
2381
|
+
this.sampleRate = options.sampleRate || 16e3;
|
|
2382
|
+
this.channels = options.channels || 1;
|
|
2383
|
+
this.events = options.events;
|
|
2384
|
+
this.volume = options.volume ?? 1;
|
|
2385
|
+
}
|
|
2386
|
+
start(_filePath) {
|
|
2387
|
+
if (this._started) return;
|
|
2388
|
+
try {
|
|
2389
|
+
const naudiodon = __require("naudiodon");
|
|
2390
|
+
const AudioOutput = naudiodon;
|
|
2391
|
+
this.audioOutput = new AudioOutput({
|
|
2392
|
+
sampleRate: this.sampleRate,
|
|
2393
|
+
channels: this.channels,
|
|
2394
|
+
bitDepth: 16
|
|
2395
|
+
});
|
|
2396
|
+
this.audioOutput.on("error", (error) => {
|
|
2397
|
+
this.handleError(error);
|
|
2398
|
+
});
|
|
2399
|
+
this.audioOutput.start();
|
|
2400
|
+
this._started = true;
|
|
2401
|
+
this._stopped = false;
|
|
2402
|
+
this.events?.onStart?.();
|
|
2403
|
+
} catch (error) {
|
|
2404
|
+
if (error.code === "MODULE_NOT_FOUND") {
|
|
2405
|
+
throw new Error("naudiodon is not installed. Run: npm install naudiodon");
|
|
2406
|
+
}
|
|
2407
|
+
throw error;
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
write(chunk) {
|
|
2411
|
+
if (!this._started || this._stopped) return;
|
|
2412
|
+
if (this.audioOutput) {
|
|
2413
|
+
const adjustedChunk = this.adjustVolume(chunk);
|
|
2414
|
+
this.audioOutput.write(adjustedChunk);
|
|
2415
|
+
this.bytesWritten += chunk.length;
|
|
2416
|
+
this.events?.onProgress?.(this.bytesWritten);
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
end() {
|
|
2420
|
+
if (this.audioOutput) {
|
|
2421
|
+
this.audioOutput.end();
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
pause() {
|
|
2425
|
+
if (!this._started || this._paused || this._stopped) return;
|
|
2426
|
+
throw new UnsupportedError("naudiodon backend does not support pause");
|
|
2427
|
+
}
|
|
2428
|
+
resume() {
|
|
2429
|
+
if (!this._paused || this._stopped) return;
|
|
2430
|
+
this._paused = false;
|
|
2431
|
+
this.events?.onResume?.();
|
|
2432
|
+
}
|
|
2433
|
+
stop() {
|
|
2434
|
+
this._stopped = true;
|
|
2435
|
+
this._started = false;
|
|
2436
|
+
this._paused = false;
|
|
2437
|
+
if (this.audioOutput) {
|
|
2438
|
+
try {
|
|
2439
|
+
this.audioOutput.quit();
|
|
2440
|
+
} catch (e) {
|
|
2441
|
+
}
|
|
2442
|
+
this.audioOutput = void 0;
|
|
2443
|
+
}
|
|
2444
|
+
this.bytesWritten = 0;
|
|
2445
|
+
this.events?.onStop?.();
|
|
2446
|
+
}
|
|
2447
|
+
getCurrentTime() {
|
|
2448
|
+
return this.bytesWritten / (this.sampleRate * this.channels * 2);
|
|
2449
|
+
}
|
|
2450
|
+
getDuration() {
|
|
2451
|
+
return void 0;
|
|
2452
|
+
}
|
|
2453
|
+
setVolume(volume) {
|
|
2454
|
+
this.volume = Math.max(0, Math.min(1, volume));
|
|
2455
|
+
}
|
|
2456
|
+
destroy() {
|
|
2457
|
+
this.stop();
|
|
2458
|
+
}
|
|
2459
|
+
adjustVolume(chunk) {
|
|
2460
|
+
if (this.volume === 1) return chunk;
|
|
2461
|
+
const adjusted = Buffer.alloc(chunk.length);
|
|
2462
|
+
for (let i = 0; i < chunk.length; i += 2) {
|
|
2463
|
+
const sample = chunk.readInt16LE(i) * this.volume;
|
|
2464
|
+
adjusted.writeInt16LE(Math.round(sample), i);
|
|
2465
|
+
}
|
|
2466
|
+
return adjusted;
|
|
2467
|
+
}
|
|
2468
|
+
handleError(error) {
|
|
2469
|
+
this.events?.onError?.(error);
|
|
2470
|
+
}
|
|
2471
|
+
};
|
|
2472
|
+
|
|
2473
|
+
// src/core/backends/afplay-backend.ts
|
|
2474
|
+
import { execFile } from "child_process";
|
|
2475
|
+
import { tmpdir } from "os";
|
|
2476
|
+
import { join } from "path";
|
|
2477
|
+
import { writeFileSync, unlinkSync, existsSync } from "fs";
|
|
2478
|
+
var SAFE_PATH_REGEX = /^[\w\/\.]+$/;
|
|
2479
|
+
var AfplayBackend = class {
|
|
2480
|
+
name = "afplay";
|
|
2481
|
+
supportsStreaming = false;
|
|
2482
|
+
process;
|
|
2483
|
+
tempFile;
|
|
2484
|
+
events;
|
|
2485
|
+
_started = false;
|
|
2486
|
+
_paused = false;
|
|
2487
|
+
_stopped = false;
|
|
2488
|
+
// P0-4: 缓冲所有chunk,等end()时一次性写入文件
|
|
2489
|
+
chunks = [];
|
|
2490
|
+
hasEnded = false;
|
|
2491
|
+
constructor(options = {}) {
|
|
2492
|
+
this.events = options.events;
|
|
2493
|
+
}
|
|
2494
|
+
start(filePath) {
|
|
2495
|
+
if (this._started) return;
|
|
2496
|
+
if (!SAFE_PATH_REGEX.test(filePath)) {
|
|
2497
|
+
throw new Error(`Invalid file path: ${filePath}`);
|
|
2498
|
+
}
|
|
2499
|
+
this.tempFile = filePath;
|
|
2500
|
+
this._started = true;
|
|
2501
|
+
this._stopped = false;
|
|
2502
|
+
this.events?.onStart?.();
|
|
2503
|
+
this.process = execFile("afplay", [filePath], (error) => {
|
|
2504
|
+
if (this._stopped) return;
|
|
2505
|
+
if (error) {
|
|
2506
|
+
this.handleError(error);
|
|
2507
|
+
return;
|
|
2508
|
+
}
|
|
2509
|
+
this._started = false;
|
|
2510
|
+
this.events?.onEnd?.();
|
|
2511
|
+
});
|
|
2512
|
+
this.process.on("error", (error) => {
|
|
2513
|
+
this.handleError(error);
|
|
2514
|
+
});
|
|
2515
|
+
}
|
|
2516
|
+
write(chunk) {
|
|
2517
|
+
if (this._stopped) return;
|
|
2518
|
+
this.chunks.push(chunk);
|
|
2519
|
+
}
|
|
2520
|
+
end() {
|
|
2521
|
+
if (this._stopped || this.hasEnded) return;
|
|
2522
|
+
this.hasEnded = true;
|
|
2523
|
+
if (this.chunks.length === 0) return;
|
|
2524
|
+
this.tempFile = join(tmpdir(), `ocosay-${Date.now()}.wav`);
|
|
2525
|
+
writeFileSync(this.tempFile, Buffer.concat(this.chunks));
|
|
2526
|
+
this.chunks = [];
|
|
2527
|
+
this.start(this.tempFile);
|
|
2528
|
+
}
|
|
2529
|
+
pause() {
|
|
2530
|
+
if (!this._started || this._paused || this._stopped) return;
|
|
2531
|
+
if (this.process) {
|
|
2532
|
+
try {
|
|
2533
|
+
this.process.kill("SIGSTOP");
|
|
2534
|
+
this._paused = true;
|
|
2535
|
+
this.events?.onPause?.();
|
|
2536
|
+
} catch (e) {
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2540
|
+
resume() {
|
|
2541
|
+
if (!this._paused || this._stopped) return;
|
|
2542
|
+
if (this.process) {
|
|
2543
|
+
try {
|
|
2544
|
+
this.process.kill("SIGCONT");
|
|
2545
|
+
this._paused = false;
|
|
2546
|
+
this.events?.onResume?.();
|
|
2547
|
+
} catch (e) {
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
stop() {
|
|
2552
|
+
this._stopped = true;
|
|
2553
|
+
this._started = false;
|
|
2554
|
+
this._paused = false;
|
|
2555
|
+
if (this.process) {
|
|
2556
|
+
try {
|
|
2557
|
+
this.process.kill("SIGTERM");
|
|
2558
|
+
} catch (e) {
|
|
2559
|
+
}
|
|
2560
|
+
this.process = void 0;
|
|
2561
|
+
}
|
|
2562
|
+
this.cleanup();
|
|
2563
|
+
this.chunks = [];
|
|
2564
|
+
this.hasEnded = false;
|
|
2565
|
+
this.events?.onStop?.();
|
|
2566
|
+
}
|
|
2567
|
+
setVolume(_volume) {
|
|
2568
|
+
}
|
|
2569
|
+
destroy() {
|
|
2570
|
+
this.stop();
|
|
2571
|
+
}
|
|
2572
|
+
cleanup() {
|
|
2573
|
+
if (this.tempFile && this.tempFile.startsWith(tmpdir())) {
|
|
2574
|
+
try {
|
|
2575
|
+
if (existsSync(this.tempFile)) {
|
|
2576
|
+
unlinkSync(this.tempFile);
|
|
2577
|
+
}
|
|
2578
|
+
} catch (e) {
|
|
2579
|
+
}
|
|
2580
|
+
this.tempFile = void 0;
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
handleError(error) {
|
|
2584
|
+
this.events?.onError?.(error);
|
|
15
2585
|
}
|
|
16
|
-
code;
|
|
17
|
-
provider;
|
|
18
|
-
details;
|
|
19
2586
|
};
|
|
20
2587
|
|
|
21
|
-
// src/
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
2588
|
+
// src/core/backends/aplay-backend.ts
|
|
2589
|
+
import { execFile as execFile2 } from "child_process";
|
|
2590
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
2591
|
+
import { join as join2 } from "path";
|
|
2592
|
+
import { writeFileSync as writeFileSync2, unlinkSync as unlinkSync2, existsSync as existsSync2 } from "fs";
|
|
2593
|
+
var SAFE_PATH_REGEX2 = /^[\w\/\.]+$/;
|
|
2594
|
+
var AplayBackend = class {
|
|
2595
|
+
name = "aplay";
|
|
2596
|
+
supportsStreaming = false;
|
|
2597
|
+
process;
|
|
2598
|
+
tempFile;
|
|
2599
|
+
events;
|
|
2600
|
+
_started = false;
|
|
2601
|
+
_paused = false;
|
|
2602
|
+
_stopped = false;
|
|
2603
|
+
// P0-4: 缓冲所有chunk,等end()时一次性写入文件
|
|
2604
|
+
chunks = [];
|
|
2605
|
+
hasEnded = false;
|
|
2606
|
+
constructor(options = {}) {
|
|
2607
|
+
this.events = options.events;
|
|
30
2608
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
)
|
|
2609
|
+
start(filePath) {
|
|
2610
|
+
if (this._started) return;
|
|
2611
|
+
if (!SAFE_PATH_REGEX2.test(filePath)) {
|
|
2612
|
+
throw new Error(`Invalid file path: ${filePath}`);
|
|
2613
|
+
}
|
|
2614
|
+
this.tempFile = filePath;
|
|
2615
|
+
this._started = true;
|
|
2616
|
+
this._stopped = false;
|
|
2617
|
+
this.events?.onStart?.();
|
|
2618
|
+
this.process = execFile2("aplay", [filePath], (error) => {
|
|
2619
|
+
if (this._stopped) return;
|
|
2620
|
+
if (error) {
|
|
2621
|
+
this.handleError(error);
|
|
2622
|
+
return;
|
|
2623
|
+
}
|
|
2624
|
+
this._started = false;
|
|
2625
|
+
this.events?.onEnd?.();
|
|
2626
|
+
});
|
|
2627
|
+
this.process.on("error", (error) => {
|
|
2628
|
+
this.handleError(error);
|
|
2629
|
+
});
|
|
41
2630
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
2631
|
+
write(chunk) {
|
|
2632
|
+
if (this._stopped) return;
|
|
2633
|
+
this.chunks.push(chunk);
|
|
2634
|
+
}
|
|
2635
|
+
end() {
|
|
2636
|
+
if (this._stopped || this.hasEnded) return;
|
|
2637
|
+
this.hasEnded = true;
|
|
2638
|
+
if (this.chunks.length === 0) return;
|
|
2639
|
+
this.tempFile = join2(tmpdir2(), `ocosay-${Date.now()}.wav`);
|
|
2640
|
+
writeFileSync2(this.tempFile, Buffer.concat(this.chunks));
|
|
2641
|
+
this.chunks = [];
|
|
2642
|
+
this.start(this.tempFile);
|
|
2643
|
+
}
|
|
2644
|
+
pause() {
|
|
2645
|
+
if (!this._started || this._paused || this._stopped) return;
|
|
2646
|
+
if (this.process) {
|
|
2647
|
+
try {
|
|
2648
|
+
this.process.kill("SIGSTOP");
|
|
2649
|
+
this._paused = true;
|
|
2650
|
+
this.events?.onPause?.();
|
|
2651
|
+
} catch (e) {
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2655
|
+
resume() {
|
|
2656
|
+
if (!this._paused || this._stopped) return;
|
|
2657
|
+
if (this.process) {
|
|
2658
|
+
try {
|
|
2659
|
+
this.process.kill("SIGCONT");
|
|
2660
|
+
this._paused = false;
|
|
2661
|
+
this.events?.onResume?.();
|
|
2662
|
+
} catch (e) {
|
|
2663
|
+
}
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
stop() {
|
|
2667
|
+
this._stopped = true;
|
|
2668
|
+
this._started = false;
|
|
2669
|
+
this._paused = false;
|
|
2670
|
+
if (this.process) {
|
|
2671
|
+
try {
|
|
2672
|
+
this.process.kill("SIGTERM");
|
|
2673
|
+
} catch (e) {
|
|
2674
|
+
}
|
|
2675
|
+
this.process = void 0;
|
|
2676
|
+
}
|
|
2677
|
+
this.cleanup();
|
|
2678
|
+
this.chunks = [];
|
|
2679
|
+
this.hasEnded = false;
|
|
2680
|
+
this.events?.onStop?.();
|
|
2681
|
+
}
|
|
2682
|
+
setVolume(_volume) {
|
|
2683
|
+
}
|
|
2684
|
+
destroy() {
|
|
2685
|
+
this.stop();
|
|
2686
|
+
}
|
|
2687
|
+
cleanup() {
|
|
2688
|
+
if (this.tempFile && this.tempFile.startsWith(tmpdir2())) {
|
|
2689
|
+
try {
|
|
2690
|
+
if (existsSync2(this.tempFile)) {
|
|
2691
|
+
unlinkSync2(this.tempFile);
|
|
2692
|
+
}
|
|
2693
|
+
} catch (e) {
|
|
2694
|
+
}
|
|
2695
|
+
this.tempFile = void 0;
|
|
2696
|
+
}
|
|
2697
|
+
}
|
|
2698
|
+
handleError(error) {
|
|
2699
|
+
this.events?.onError?.(error);
|
|
2700
|
+
}
|
|
2701
|
+
};
|
|
2702
|
+
|
|
2703
|
+
// src/core/backends/powershell-backend.ts
|
|
2704
|
+
import { spawn } from "child_process";
|
|
2705
|
+
import { tmpdir as tmpdir3 } from "os";
|
|
2706
|
+
import { join as join3 } from "path";
|
|
2707
|
+
import { writeFileSync as writeFileSync3, unlinkSync as unlinkSync3, existsSync as existsSync3 } from "fs";
|
|
2708
|
+
var SAFE_PATH_REGEX3 = /^[\w\:\\_.]+$/i;
|
|
2709
|
+
function wslPathToWindows(wslPath) {
|
|
2710
|
+
return wslPath.replace(/^\/mnt\/([a-z])\//, "$1:/").replace(/\//g, "\\");
|
|
49
2711
|
}
|
|
50
|
-
var
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
2712
|
+
var UnsupportedError2 = class extends Error {
|
|
2713
|
+
constructor(message) {
|
|
2714
|
+
super(message);
|
|
2715
|
+
this.name = "UnsupportedError";
|
|
2716
|
+
}
|
|
2717
|
+
};
|
|
2718
|
+
var PowerShellBackend = class {
|
|
2719
|
+
name = "powershell";
|
|
2720
|
+
supportsStreaming = false;
|
|
2721
|
+
process;
|
|
2722
|
+
tempFile;
|
|
2723
|
+
events;
|
|
2724
|
+
_started = false;
|
|
2725
|
+
_paused = false;
|
|
2726
|
+
_stopped = false;
|
|
2727
|
+
// P0-4: 缓冲所有chunk,等end()时一次性写入文件
|
|
2728
|
+
chunks = [];
|
|
2729
|
+
hasEnded = false;
|
|
2730
|
+
constructor(options = {}) {
|
|
2731
|
+
this.events = options.events;
|
|
2732
|
+
}
|
|
2733
|
+
start(filePath) {
|
|
2734
|
+
if (this._started) return;
|
|
2735
|
+
if (isWsl()) {
|
|
2736
|
+
filePath = wslPathToWindows(filePath);
|
|
2737
|
+
}
|
|
2738
|
+
if (!SAFE_PATH_REGEX3.test(filePath)) {
|
|
2739
|
+
throw new Error(`Invalid file path: ${filePath}`);
|
|
2740
|
+
}
|
|
2741
|
+
this.tempFile = filePath;
|
|
2742
|
+
this._started = true;
|
|
2743
|
+
this._stopped = false;
|
|
2744
|
+
this.events?.onStart?.();
|
|
2745
|
+
const escapedPath = filePath.replace(/'/g, "''");
|
|
2746
|
+
const psScript = `$sound = New-Object System.Media.SoundPlayer('${escapedPath}'); $sound.PlayAsync()`;
|
|
2747
|
+
const scriptFile = join3(tmpdir3(), `ocosay-${Date.now()}.ps1`);
|
|
2748
|
+
writeFileSync3(scriptFile, psScript, { encoding: "utf8" });
|
|
2749
|
+
this.process = spawn("powershell", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", scriptFile], {
|
|
2750
|
+
stdio: "ignore",
|
|
2751
|
+
detached: false
|
|
2752
|
+
});
|
|
2753
|
+
this.process.on("exit", () => {
|
|
2754
|
+
try {
|
|
2755
|
+
if (existsSync3(scriptFile)) {
|
|
2756
|
+
unlinkSync3(scriptFile);
|
|
2757
|
+
}
|
|
2758
|
+
} catch (e) {
|
|
2759
|
+
}
|
|
2760
|
+
});
|
|
2761
|
+
this.process.on("exit", (code) => {
|
|
2762
|
+
if (this._stopped) return;
|
|
2763
|
+
if (code === 0 || code === null) {
|
|
2764
|
+
this._started = false;
|
|
2765
|
+
this.events?.onEnd?.();
|
|
2766
|
+
} else {
|
|
2767
|
+
this.handleError(new Error(`PowerShell playback exited with code ${code}`));
|
|
2768
|
+
}
|
|
2769
|
+
});
|
|
2770
|
+
this.process.on("error", (error) => {
|
|
2771
|
+
this.handleError(error);
|
|
2772
|
+
});
|
|
2773
|
+
}
|
|
2774
|
+
write(chunk) {
|
|
2775
|
+
if (this._stopped) return;
|
|
2776
|
+
this.chunks.push(chunk);
|
|
2777
|
+
}
|
|
2778
|
+
end() {
|
|
2779
|
+
if (this._stopped || this.hasEnded) return;
|
|
2780
|
+
this.hasEnded = true;
|
|
2781
|
+
if (this.chunks.length === 0) return;
|
|
2782
|
+
this.tempFile = join3(tmpdir3(), `ocosay-${Date.now()}.wav`);
|
|
2783
|
+
writeFileSync3(this.tempFile, Buffer.concat(this.chunks));
|
|
2784
|
+
this.chunks = [];
|
|
2785
|
+
this.start(this.tempFile);
|
|
2786
|
+
}
|
|
2787
|
+
pause() {
|
|
2788
|
+
if (!this._started || this._paused || this._stopped) return;
|
|
2789
|
+
throw new UnsupportedError2("pause is not supported by PowerShell SoundPlayer");
|
|
2790
|
+
}
|
|
2791
|
+
resume() {
|
|
2792
|
+
if (!this._paused || this._stopped) return;
|
|
2793
|
+
throw new UnsupportedError2("resume is not supported by PowerShell SoundPlayer");
|
|
2794
|
+
}
|
|
2795
|
+
stop() {
|
|
2796
|
+
this._stopped = true;
|
|
2797
|
+
this._started = false;
|
|
2798
|
+
this._paused = false;
|
|
2799
|
+
if (this.process) {
|
|
2800
|
+
try {
|
|
2801
|
+
this.process.kill("SIGTERM");
|
|
2802
|
+
} catch (e) {
|
|
2803
|
+
}
|
|
2804
|
+
this.process = void 0;
|
|
2805
|
+
}
|
|
2806
|
+
this.cleanup();
|
|
2807
|
+
this.chunks = [];
|
|
2808
|
+
this.hasEnded = false;
|
|
2809
|
+
this.events?.onStop?.();
|
|
2810
|
+
}
|
|
2811
|
+
setVolume(_volume) {
|
|
2812
|
+
}
|
|
2813
|
+
destroy() {
|
|
2814
|
+
this.stop();
|
|
2815
|
+
}
|
|
2816
|
+
cleanup() {
|
|
2817
|
+
if (this.tempFile && this.tempFile.startsWith(tmpdir3())) {
|
|
2818
|
+
try {
|
|
2819
|
+
if (existsSync3(this.tempFile)) {
|
|
2820
|
+
unlinkSync3(this.tempFile);
|
|
2821
|
+
}
|
|
2822
|
+
} catch (e) {
|
|
2823
|
+
}
|
|
2824
|
+
this.tempFile = void 0;
|
|
2825
|
+
}
|
|
55
2826
|
}
|
|
56
|
-
|
|
2827
|
+
handleError(error) {
|
|
2828
|
+
this.events?.onError?.(error);
|
|
57
2829
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
2830
|
+
};
|
|
2831
|
+
|
|
2832
|
+
// src/core/backends/howler-backend.ts
|
|
2833
|
+
var import_howler = __toESM(require_howler(), 1);
|
|
2834
|
+
var HowlerBackend = class {
|
|
2835
|
+
name = "howler";
|
|
2836
|
+
supportsStreaming = false;
|
|
2837
|
+
// Howler.js 不支持真正的流式播放
|
|
2838
|
+
howl = null;
|
|
2839
|
+
events;
|
|
2840
|
+
_started = false;
|
|
2841
|
+
_paused = false;
|
|
2842
|
+
_stopped = false;
|
|
2843
|
+
volume = 1;
|
|
2844
|
+
bytesWritten = 0;
|
|
2845
|
+
blobUrl = null;
|
|
2846
|
+
format;
|
|
2847
|
+
constructor(options = {}) {
|
|
2848
|
+
this.events = options.events;
|
|
2849
|
+
this.volume = options.volume ?? 1;
|
|
2850
|
+
this.format = options.format || "mp3";
|
|
2851
|
+
}
|
|
2852
|
+
start(_filePath) {
|
|
2853
|
+
if (this._started) {
|
|
2854
|
+
this.stop();
|
|
2855
|
+
}
|
|
2856
|
+
this._started = true;
|
|
2857
|
+
this._stopped = false;
|
|
2858
|
+
this._paused = false;
|
|
2859
|
+
this.bytesWritten = 0;
|
|
2860
|
+
this.howl = new import_howler.Howl({
|
|
2861
|
+
src: [""],
|
|
2862
|
+
// 初始为空
|
|
2863
|
+
html5: true,
|
|
2864
|
+
// 启用 HTML5 Audio 模式支持流式
|
|
2865
|
+
volume: this.volume,
|
|
2866
|
+
format: [this.format],
|
|
2867
|
+
onplay: () => {
|
|
2868
|
+
this.events?.onStart?.();
|
|
2869
|
+
},
|
|
2870
|
+
onpause: () => {
|
|
2871
|
+
if (!this._stopped) {
|
|
2872
|
+
this.events?.onPause?.();
|
|
2873
|
+
}
|
|
2874
|
+
},
|
|
2875
|
+
onstop: () => {
|
|
2876
|
+
if (!this._stopped) {
|
|
2877
|
+
this.events?.onStop?.();
|
|
2878
|
+
}
|
|
2879
|
+
},
|
|
2880
|
+
onend: () => {
|
|
2881
|
+
this.events?.onEnd?.();
|
|
2882
|
+
},
|
|
2883
|
+
onloaderror: (_id, error) => {
|
|
2884
|
+
this.handleError(new Error(`Howler load error: ${error}`));
|
|
2885
|
+
},
|
|
2886
|
+
onplayerror: (_id, error) => {
|
|
2887
|
+
this.handleError(new Error(`Howler play error: ${error}`));
|
|
2888
|
+
}
|
|
2889
|
+
});
|
|
2890
|
+
}
|
|
2891
|
+
write(chunk) {
|
|
2892
|
+
if (!this._started || this._stopped) return;
|
|
2893
|
+
if (!this.howl) return;
|
|
2894
|
+
this.bytesWritten += chunk.length;
|
|
2895
|
+
this.events?.onProgress?.(this.bytesWritten);
|
|
2896
|
+
const blob = new Blob([chunk], { type: `audio/${this.format}` });
|
|
2897
|
+
if (this.blobUrl) {
|
|
2898
|
+
URL.revokeObjectURL(this.blobUrl);
|
|
2899
|
+
}
|
|
2900
|
+
this.blobUrl = URL.createObjectURL(blob);
|
|
2901
|
+
const wasPlaying = this.howl.playing();
|
|
2902
|
+
this.howl.stop();
|
|
2903
|
+
this.howl.unload();
|
|
2904
|
+
this.howl = new import_howler.Howl({
|
|
2905
|
+
src: [this.blobUrl],
|
|
2906
|
+
html5: true,
|
|
2907
|
+
volume: this.volume,
|
|
2908
|
+
format: [this.format],
|
|
2909
|
+
onplay: () => {
|
|
2910
|
+
if (!wasPlaying) {
|
|
2911
|
+
this.events?.onStart?.();
|
|
2912
|
+
}
|
|
2913
|
+
},
|
|
2914
|
+
onpause: () => {
|
|
2915
|
+
if (!this._stopped) {
|
|
2916
|
+
this.events?.onPause?.();
|
|
2917
|
+
}
|
|
2918
|
+
},
|
|
2919
|
+
onstop: () => {
|
|
2920
|
+
if (!this._stopped) {
|
|
2921
|
+
this.events?.onStop?.();
|
|
2922
|
+
}
|
|
2923
|
+
},
|
|
2924
|
+
onend: () => {
|
|
2925
|
+
this.events?.onEnd?.();
|
|
2926
|
+
},
|
|
2927
|
+
onloaderror: (_id, error) => {
|
|
2928
|
+
this.handleError(new Error(`Howler load error: ${error}`));
|
|
2929
|
+
},
|
|
2930
|
+
onplayerror: (_id, error) => {
|
|
2931
|
+
this.handleError(new Error(`Howler play error: ${error}`));
|
|
2932
|
+
}
|
|
2933
|
+
});
|
|
2934
|
+
if (wasPlaying || this._started) {
|
|
2935
|
+
this.howl.play();
|
|
68
2936
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
return this.doSpeak(text, voice, model, options);
|
|
2937
|
+
}
|
|
2938
|
+
end() {
|
|
72
2939
|
}
|
|
73
2940
|
pause() {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
this.
|
|
78
|
-
|
|
2941
|
+
if (!this._started || this._paused || this._stopped) return;
|
|
2942
|
+
if (this.howl && this.howl.playing()) {
|
|
2943
|
+
this.howl.pause();
|
|
2944
|
+
this._paused = true;
|
|
2945
|
+
this.events?.onPause?.();
|
|
2946
|
+
}
|
|
79
2947
|
}
|
|
80
2948
|
resume() {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
this.
|
|
85
|
-
|
|
2949
|
+
if (!this._paused || this._stopped) return;
|
|
2950
|
+
if (this.howl) {
|
|
2951
|
+
this.howl.play();
|
|
2952
|
+
this._paused = false;
|
|
2953
|
+
this.events?.onResume?.();
|
|
2954
|
+
}
|
|
86
2955
|
}
|
|
87
2956
|
stop() {
|
|
88
|
-
|
|
2957
|
+
this._stopped = true;
|
|
2958
|
+
this._started = false;
|
|
2959
|
+
this._paused = false;
|
|
2960
|
+
if (this.howl) {
|
|
2961
|
+
try {
|
|
2962
|
+
this.howl.stop();
|
|
2963
|
+
this.howl.unload();
|
|
2964
|
+
} catch (e) {
|
|
2965
|
+
}
|
|
2966
|
+
this.howl = null;
|
|
2967
|
+
}
|
|
2968
|
+
if (this.blobUrl) {
|
|
2969
|
+
URL.revokeObjectURL(this.blobUrl);
|
|
2970
|
+
this.blobUrl = null;
|
|
2971
|
+
}
|
|
2972
|
+
this.bytesWritten = 0;
|
|
2973
|
+
this.events?.onStop?.();
|
|
89
2974
|
}
|
|
90
|
-
|
|
91
|
-
|
|
2975
|
+
getCurrentTime() {
|
|
2976
|
+
if (this.howl) {
|
|
2977
|
+
return this.howl.seek();
|
|
2978
|
+
}
|
|
2979
|
+
return void 0;
|
|
92
2980
|
}
|
|
93
|
-
|
|
94
|
-
|
|
2981
|
+
getDuration() {
|
|
2982
|
+
if (this.howl) {
|
|
2983
|
+
return this.howl.duration();
|
|
2984
|
+
}
|
|
2985
|
+
return void 0;
|
|
95
2986
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if (!this.apiKey) {
|
|
101
|
-
throw new TTSError(
|
|
102
|
-
`API key is required for provider "${this.name}"`,
|
|
103
|
-
"AUTH" /* AUTH */,
|
|
104
|
-
this.name
|
|
105
|
-
);
|
|
2987
|
+
setVolume(volume) {
|
|
2988
|
+
this.volume = Math.max(0, Math.min(1, volume));
|
|
2989
|
+
if (this.howl) {
|
|
2990
|
+
this.howl.volume(this.volume);
|
|
106
2991
|
}
|
|
107
2992
|
}
|
|
2993
|
+
destroy() {
|
|
2994
|
+
this.stop();
|
|
2995
|
+
}
|
|
2996
|
+
handleError(error) {
|
|
2997
|
+
this.events?.onError?.(error);
|
|
2998
|
+
}
|
|
108
2999
|
};
|
|
109
3000
|
|
|
3001
|
+
// src/core/backends/index.ts
|
|
3002
|
+
function isNaudiodonAvailable() {
|
|
3003
|
+
try {
|
|
3004
|
+
__require.resolve("naudiodon");
|
|
3005
|
+
return true;
|
|
3006
|
+
} catch (e) {
|
|
3007
|
+
return false;
|
|
3008
|
+
}
|
|
3009
|
+
}
|
|
3010
|
+
function isWsl() {
|
|
3011
|
+
if (process.platform !== "linux") return false;
|
|
3012
|
+
try {
|
|
3013
|
+
return __require("fs").readFileSync("/proc/version", "utf8").toLowerCase().includes("microsoft");
|
|
3014
|
+
} catch {
|
|
3015
|
+
return false;
|
|
3016
|
+
}
|
|
3017
|
+
}
|
|
3018
|
+
function createBackend(type = "auto" /* AUTO */, options = {}) {
|
|
3019
|
+
const platform = process.platform;
|
|
3020
|
+
if (type !== "auto" /* AUTO */) {
|
|
3021
|
+
return createBackendByType(type, options);
|
|
3022
|
+
}
|
|
3023
|
+
if (isNaudiodonAvailable()) {
|
|
3024
|
+
try {
|
|
3025
|
+
const naudiodon = __require("naudiodon");
|
|
3026
|
+
if (naudiodon) {
|
|
3027
|
+
return new NaudiodonBackend(options);
|
|
3028
|
+
}
|
|
3029
|
+
} catch (e) {
|
|
3030
|
+
}
|
|
3031
|
+
}
|
|
3032
|
+
switch (platform) {
|
|
3033
|
+
case "darwin":
|
|
3034
|
+
return new AfplayBackend(options);
|
|
3035
|
+
case "linux":
|
|
3036
|
+
if (isWsl()) {
|
|
3037
|
+
return new PowerShellBackend(options);
|
|
3038
|
+
}
|
|
3039
|
+
return new AplayBackend(options);
|
|
3040
|
+
case "win32":
|
|
3041
|
+
return new PowerShellBackend(options);
|
|
3042
|
+
default:
|
|
3043
|
+
throw new Error(`Unsupported platform: ${platform}`);
|
|
3044
|
+
}
|
|
3045
|
+
}
|
|
3046
|
+
function createBackendByType(type, options) {
|
|
3047
|
+
switch (type) {
|
|
3048
|
+
case "naudiodon" /* NAUDIODON */:
|
|
3049
|
+
return new NaudiodonBackend(options);
|
|
3050
|
+
case "afplay" /* AFPLAY */:
|
|
3051
|
+
return new AfplayBackend(options);
|
|
3052
|
+
case "aplay" /* APLAY */:
|
|
3053
|
+
return new AplayBackend(options);
|
|
3054
|
+
case "powershell" /* POWERSHELL */:
|
|
3055
|
+
return new PowerShellBackend(options);
|
|
3056
|
+
case "howler" /* HOWLER */:
|
|
3057
|
+
return new HowlerBackend(options);
|
|
3058
|
+
default:
|
|
3059
|
+
throw new Error(`Unknown backend type: ${type}`);
|
|
3060
|
+
}
|
|
3061
|
+
}
|
|
3062
|
+
|
|
110
3063
|
// src/core/player.ts
|
|
111
|
-
import { EventEmitter } from "events";
|
|
112
|
-
import { spawn } from "child_process";
|
|
113
|
-
import fs from "fs";
|
|
114
|
-
import { createWriteStream } from "fs";
|
|
115
|
-
import { tmpdir } from "os";
|
|
116
|
-
import { join } from "path";
|
|
117
3064
|
var AudioPlayer = class extends EventEmitter {
|
|
118
3065
|
constructor(events) {
|
|
119
3066
|
super();
|
|
120
3067
|
this.events = events;
|
|
3068
|
+
this.backend = createBackend("auto" /* AUTO */, {
|
|
3069
|
+
events: {
|
|
3070
|
+
onStart: () => {
|
|
3071
|
+
this.events?.onStart?.();
|
|
3072
|
+
this.emit("start");
|
|
3073
|
+
},
|
|
3074
|
+
onEnd: () => {
|
|
3075
|
+
this._playing = false;
|
|
3076
|
+
this.events?.onEnd?.();
|
|
3077
|
+
this.emit("end");
|
|
3078
|
+
this.cleanup();
|
|
3079
|
+
},
|
|
3080
|
+
onError: (error) => {
|
|
3081
|
+
this._playing = false;
|
|
3082
|
+
const ttsError = new TTSError(
|
|
3083
|
+
error.message || "Playback failed",
|
|
3084
|
+
"PLAYER_ERROR" /* PLAYER_ERROR */,
|
|
3085
|
+
"player"
|
|
3086
|
+
);
|
|
3087
|
+
this.events?.onError?.(ttsError);
|
|
3088
|
+
this.emit("error", ttsError);
|
|
3089
|
+
},
|
|
3090
|
+
onPause: () => {
|
|
3091
|
+
this.events?.onPause?.();
|
|
3092
|
+
this.emit("pause");
|
|
3093
|
+
},
|
|
3094
|
+
onResume: () => {
|
|
3095
|
+
this.events?.onResume?.();
|
|
3096
|
+
this.emit("resume");
|
|
3097
|
+
},
|
|
3098
|
+
onStop: () => {
|
|
3099
|
+
this.events?.onStop?.();
|
|
3100
|
+
this.emit("stop");
|
|
3101
|
+
}
|
|
3102
|
+
}
|
|
3103
|
+
});
|
|
121
3104
|
}
|
|
122
3105
|
events;
|
|
123
3106
|
_playing = false;
|
|
124
3107
|
_paused = false;
|
|
125
|
-
|
|
3108
|
+
backend;
|
|
126
3109
|
currentFile;
|
|
127
3110
|
/**
|
|
128
3111
|
* 播放音频
|
|
@@ -136,7 +3119,7 @@ var AudioPlayer = class extends EventEmitter {
|
|
|
136
3119
|
this._playing = true;
|
|
137
3120
|
this._paused = false;
|
|
138
3121
|
try {
|
|
139
|
-
const tempFile =
|
|
3122
|
+
const tempFile = join4(tmpdir4(), `ocosay-${Date.now()}.${format}`);
|
|
140
3123
|
this.currentFile = tempFile;
|
|
141
3124
|
if (Buffer.isBuffer(audioData)) {
|
|
142
3125
|
fs.writeFileSync(tempFile, audioData);
|
|
@@ -181,88 +3164,29 @@ var AudioPlayer = class extends EventEmitter {
|
|
|
181
3164
|
}
|
|
182
3165
|
/**
|
|
183
3166
|
* 播放音频文件
|
|
184
|
-
*
|
|
3167
|
+
* 使用 AudioBackend 统一后端播放
|
|
185
3168
|
*/
|
|
186
|
-
playFile(filePath,
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
let args;
|
|
191
|
-
if (platform === "darwin") {
|
|
192
|
-
command = "afplay";
|
|
193
|
-
args = [filePath];
|
|
194
|
-
} else if (platform === "linux") {
|
|
195
|
-
command = "aplay";
|
|
196
|
-
args = [filePath];
|
|
197
|
-
} else {
|
|
198
|
-
command = "powershell";
|
|
199
|
-
args = ["-c", `(New-Object System.Media.SoundPlayer('${filePath.replace(/\\/g, "\\\\")}')).PlaySync()`];
|
|
200
|
-
}
|
|
201
|
-
this.currentProcess = spawn(command, args, {
|
|
202
|
-
stdio: "ignore",
|
|
203
|
-
detached: false
|
|
204
|
-
});
|
|
205
|
-
this.currentProcess.on("exit", (code, signal) => {
|
|
206
|
-
if (signal === "SIGTERM" || signal === "SIGINT") {
|
|
207
|
-
resolve();
|
|
208
|
-
} else if (code === 0) {
|
|
209
|
-
resolve();
|
|
210
|
-
} else {
|
|
211
|
-
reject(new Error(`Player exited with code ${code}`));
|
|
212
|
-
}
|
|
213
|
-
});
|
|
214
|
-
this.currentProcess.on("error", (error) => {
|
|
215
|
-
reject(error);
|
|
216
|
-
});
|
|
3169
|
+
playFile(filePath, _format) {
|
|
3170
|
+
return new Promise((resolve) => {
|
|
3171
|
+
this.backend.start(filePath);
|
|
3172
|
+
resolve();
|
|
217
3173
|
});
|
|
218
3174
|
}
|
|
219
|
-
/**
|
|
220
|
-
* 暂停播放
|
|
221
|
-
* 注意: 目前通过 SIGSTOP 实现,真正的 pause 需要支持暂停的音频库
|
|
222
|
-
*/
|
|
223
3175
|
pause() {
|
|
224
3176
|
if (!this._playing || this._paused) return;
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
this.currentProcess.kill("SIGSTOP");
|
|
228
|
-
this._paused = true;
|
|
229
|
-
this.events?.onPause?.();
|
|
230
|
-
this.emit("pause");
|
|
231
|
-
} catch (e) {
|
|
232
|
-
}
|
|
233
|
-
}
|
|
3177
|
+
this.backend.pause();
|
|
3178
|
+
this._paused = true;
|
|
234
3179
|
}
|
|
235
|
-
/**
|
|
236
|
-
* 恢复播放
|
|
237
|
-
*/
|
|
238
3180
|
resume() {
|
|
239
3181
|
if (!this._playing || !this._paused) return;
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
this.currentProcess.kill("SIGCONT");
|
|
243
|
-
this._paused = false;
|
|
244
|
-
this.events?.onResume?.();
|
|
245
|
-
this.emit("resume");
|
|
246
|
-
} catch (e) {
|
|
247
|
-
}
|
|
248
|
-
}
|
|
3182
|
+
this.backend.resume();
|
|
3183
|
+
this._paused = false;
|
|
249
3184
|
}
|
|
250
|
-
/**
|
|
251
|
-
* 停止播放
|
|
252
|
-
*/
|
|
253
3185
|
async stop() {
|
|
254
3186
|
this._playing = false;
|
|
255
3187
|
this._paused = false;
|
|
256
|
-
|
|
257
|
-
try {
|
|
258
|
-
this.currentProcess.kill("SIGTERM");
|
|
259
|
-
} catch (e) {
|
|
260
|
-
}
|
|
261
|
-
this.currentProcess = void 0;
|
|
262
|
-
}
|
|
3188
|
+
this.backend.stop();
|
|
263
3189
|
this.cleanup();
|
|
264
|
-
this.events?.onStop?.();
|
|
265
|
-
this.emit("stop");
|
|
266
3190
|
}
|
|
267
3191
|
/**
|
|
268
3192
|
* 是否正在播放
|
|
@@ -523,6 +3447,20 @@ async function listVoices(providerName) {
|
|
|
523
3447
|
}
|
|
524
3448
|
|
|
525
3449
|
// src/tools/tts.ts
|
|
3450
|
+
function extractTextArg(args) {
|
|
3451
|
+
if (!args || typeof args !== "object") {
|
|
3452
|
+
return void 0;
|
|
3453
|
+
}
|
|
3454
|
+
const argObj = args;
|
|
3455
|
+
const text = argObj.text;
|
|
3456
|
+
if (typeof text === "string" && text.trim().length > 0) {
|
|
3457
|
+
return text;
|
|
3458
|
+
}
|
|
3459
|
+
if (text !== void 0) {
|
|
3460
|
+
console.warn("[tts] text arg is not a valid string, type:", typeof text, "value:", String(text).substring(0, 100));
|
|
3461
|
+
}
|
|
3462
|
+
return void 0;
|
|
3463
|
+
}
|
|
526
3464
|
async function handleToolCall(toolName, args) {
|
|
527
3465
|
try {
|
|
528
3466
|
switch (toolName) {
|
|
@@ -545,21 +3483,24 @@ async function handleToolCall(toolName, args) {
|
|
|
545
3483
|
case "tts_resume":
|
|
546
3484
|
resume();
|
|
547
3485
|
return { success: true, message: "Resumed" };
|
|
548
|
-
case "tts_list_voices":
|
|
3486
|
+
case "tts_list_voices": {
|
|
549
3487
|
const voices = await listVoices(args?.provider);
|
|
550
3488
|
return { success: true, voices };
|
|
551
|
-
|
|
3489
|
+
}
|
|
3490
|
+
case "tts_list_providers": {
|
|
552
3491
|
const speaker2 = getDefaultSpeaker();
|
|
553
3492
|
const providers2 = speaker2.getProviders();
|
|
554
3493
|
return { success: true, providers: providers2 };
|
|
555
|
-
|
|
3494
|
+
}
|
|
3495
|
+
case "tts_status": {
|
|
556
3496
|
const s = getDefaultSpeaker();
|
|
557
3497
|
return {
|
|
558
3498
|
success: true,
|
|
559
3499
|
isPlaying: s.isPlaying(),
|
|
560
3500
|
isPaused: s.isPausedState()
|
|
561
3501
|
};
|
|
562
|
-
|
|
3502
|
+
}
|
|
3503
|
+
case "tts_stream_speak": {
|
|
563
3504
|
if (!isAutoReadEnabled()) {
|
|
564
3505
|
throw new TTSError(
|
|
565
3506
|
"Stream mode is not enabled. autoRead must be enabled in configuration to use tts_stream_speak.",
|
|
@@ -578,11 +3519,10 @@ async function handleToolCall(toolName, args) {
|
|
|
578
3519
|
const synthesizer = getStreamingSynthesizer();
|
|
579
3520
|
if (streamReader2 && synthesizer) {
|
|
580
3521
|
streamReader2.start();
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
console.log("[tts_stream_speak] args.text is not a string, type:", typeof args.text, "value:", args.text);
|
|
3522
|
+
const textArg = extractTextArg(args);
|
|
3523
|
+
if (textArg && typeof textArg === "string" && textArg.trim().length > 0) {
|
|
3524
|
+
console.log("[tts_stream_speak] synthesizing text:", textArg.substring(0, 50) + "...");
|
|
3525
|
+
synthesizer.synthesize(textArg);
|
|
586
3526
|
}
|
|
587
3527
|
return { success: true, message: "Stream speak started" };
|
|
588
3528
|
}
|
|
@@ -591,7 +3531,8 @@ async function handleToolCall(toolName, args) {
|
|
|
591
3531
|
"UNKNOWN" /* UNKNOWN */,
|
|
592
3532
|
"tts_stream"
|
|
593
3533
|
);
|
|
594
|
-
|
|
3534
|
+
}
|
|
3535
|
+
case "tts_stream_stop": {
|
|
595
3536
|
if (!isStreamEnabled()) {
|
|
596
3537
|
throw new TTSError(
|
|
597
3538
|
"Stream mode is not enabled. Please enable autoRead in configuration.",
|
|
@@ -609,6 +3550,7 @@ async function handleToolCall(toolName, args) {
|
|
|
609
3550
|
"UNKNOWN" /* UNKNOWN */,
|
|
610
3551
|
"tts_stream"
|
|
611
3552
|
);
|
|
3553
|
+
}
|
|
612
3554
|
case "tts_stream_status":
|
|
613
3555
|
if (!isStreamEnabled()) {
|
|
614
3556
|
return {
|
|
@@ -1310,15 +4252,8 @@ var StreamingSynthesizer = class extends EventEmitter4 {
|
|
|
1310
4252
|
|
|
1311
4253
|
// src/core/stream-player.ts
|
|
1312
4254
|
import { EventEmitter as EventEmitter5 } from "events";
|
|
1313
|
-
import { spawn as spawn2 } from "child_process";
|
|
1314
|
-
import fs2 from "fs";
|
|
1315
|
-
import { createWriteStream as createWriteStream2 } from "fs";
|
|
1316
|
-
import { tmpdir as tmpdir2 } from "os";
|
|
1317
|
-
import { join as join2 } from "path";
|
|
1318
4255
|
var StreamPlayer = class extends EventEmitter5 {
|
|
1319
|
-
|
|
1320
|
-
writeStream;
|
|
1321
|
-
playerProcess;
|
|
4256
|
+
backend = null;
|
|
1322
4257
|
_bytesWritten = 0;
|
|
1323
4258
|
_started = false;
|
|
1324
4259
|
_paused = false;
|
|
@@ -1329,69 +4264,62 @@ var StreamPlayer = class extends EventEmitter5 {
|
|
|
1329
4264
|
super();
|
|
1330
4265
|
this.format = options.format || "mp3";
|
|
1331
4266
|
this.events = options.events;
|
|
4267
|
+
const backendType = options.backendType || "naudiodon" /* NAUDIODON */;
|
|
4268
|
+
this.backend = createBackend(backendType, {
|
|
4269
|
+
format: this.format,
|
|
4270
|
+
events: {
|
|
4271
|
+
onStart: () => {
|
|
4272
|
+
this.events?.onStart?.();
|
|
4273
|
+
this.emit("start");
|
|
4274
|
+
},
|
|
4275
|
+
onEnd: () => {
|
|
4276
|
+
this.events?.onEnd?.();
|
|
4277
|
+
this.emit("end");
|
|
4278
|
+
},
|
|
4279
|
+
onError: (error) => {
|
|
4280
|
+
this.handleError(error);
|
|
4281
|
+
},
|
|
4282
|
+
onPause: () => {
|
|
4283
|
+
this._paused = true;
|
|
4284
|
+
this.events?.onPause?.();
|
|
4285
|
+
this.emit("pause");
|
|
4286
|
+
},
|
|
4287
|
+
onResume: () => {
|
|
4288
|
+
this._paused = false;
|
|
4289
|
+
this.events?.onResume?.();
|
|
4290
|
+
this.emit("resume");
|
|
4291
|
+
},
|
|
4292
|
+
onStop: () => {
|
|
4293
|
+
this.events?.onStop?.();
|
|
4294
|
+
this.emit("stop");
|
|
4295
|
+
},
|
|
4296
|
+
onProgress: (bytes) => {
|
|
4297
|
+
this.events?.onProgress?.(bytes);
|
|
4298
|
+
this.emit("progress", bytes);
|
|
4299
|
+
}
|
|
4300
|
+
}
|
|
4301
|
+
});
|
|
1332
4302
|
}
|
|
1333
4303
|
/**
|
|
1334
4304
|
* 开始播放
|
|
1335
|
-
*
|
|
4305
|
+
* 初始化后端,准备接收音频数据
|
|
1336
4306
|
*/
|
|
1337
4307
|
start() {
|
|
1338
4308
|
if (this._started) {
|
|
1339
4309
|
return;
|
|
1340
4310
|
}
|
|
1341
|
-
this.
|
|
1342
|
-
|
|
1343
|
-
this.writeStream.on("error", (error) => {
|
|
1344
|
-
this.handleError(error);
|
|
1345
|
-
});
|
|
1346
|
-
this.writeStream.on("finish", () => {
|
|
1347
|
-
});
|
|
1348
|
-
this.startPlayer();
|
|
1349
|
-
this._started = true;
|
|
1350
|
-
this._stopped = false;
|
|
1351
|
-
this.events?.onStart?.();
|
|
1352
|
-
this.emit("start");
|
|
1353
|
-
}
|
|
1354
|
-
/**
|
|
1355
|
-
* 启动播放器进程
|
|
1356
|
-
*/
|
|
1357
|
-
startPlayer() {
|
|
1358
|
-
const platform = process.platform;
|
|
1359
|
-
let command;
|
|
1360
|
-
let args;
|
|
1361
|
-
if (platform === "darwin") {
|
|
1362
|
-
command = "afplay";
|
|
1363
|
-
args = [this.tempFile];
|
|
1364
|
-
} else if (platform === "linux") {
|
|
1365
|
-
command = "aplay";
|
|
1366
|
-
args = [this.tempFile];
|
|
1367
|
-
} else {
|
|
1368
|
-
this.handleError(new Error("Windows platform is not supported for stream playback. PlaySync() blocks the Node.js event loop."));
|
|
4311
|
+
if (!this.backend) {
|
|
4312
|
+
this.handleError(new Error("Audio backend not initialized"));
|
|
1369
4313
|
return;
|
|
1370
4314
|
}
|
|
1371
|
-
this.
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
this.
|
|
1376
|
-
if (this._stopped) {
|
|
1377
|
-
return;
|
|
1378
|
-
}
|
|
1379
|
-
if (signal === "SIGTERM" || signal === "SIGINT") {
|
|
1380
|
-
return;
|
|
1381
|
-
}
|
|
1382
|
-
if (code === 0 || code === null) {
|
|
1383
|
-
this.events?.onEnd?.();
|
|
1384
|
-
this.emit("end");
|
|
1385
|
-
} else {
|
|
1386
|
-
this.handleError(new Error(`Player exited with code ${code}`));
|
|
1387
|
-
}
|
|
1388
|
-
});
|
|
1389
|
-
this.playerProcess.on("error", (error) => {
|
|
1390
|
-
this.handleError(error);
|
|
1391
|
-
});
|
|
4315
|
+
this.backend.start("");
|
|
4316
|
+
this._started = true;
|
|
4317
|
+
this._stopped = false;
|
|
4318
|
+
this._paused = false;
|
|
4319
|
+
this._bytesWritten = 0;
|
|
1392
4320
|
}
|
|
1393
4321
|
/**
|
|
1394
|
-
*
|
|
4322
|
+
* 写入音频数据块(边收边播)
|
|
1395
4323
|
* 如果尚未 start(),会自动调用
|
|
1396
4324
|
*/
|
|
1397
4325
|
write(chunk) {
|
|
@@ -1401,12 +4329,8 @@ var StreamPlayer = class extends EventEmitter5 {
|
|
|
1401
4329
|
if (!this._started) {
|
|
1402
4330
|
this.start();
|
|
1403
4331
|
}
|
|
1404
|
-
if (this.
|
|
1405
|
-
|
|
1406
|
-
if (!canContinue) {
|
|
1407
|
-
this.writeStream.once("drain", () => {
|
|
1408
|
-
});
|
|
1409
|
-
}
|
|
4332
|
+
if (this.backend) {
|
|
4333
|
+
this.backend.write(chunk);
|
|
1410
4334
|
this._bytesWritten += chunk.length;
|
|
1411
4335
|
this.events?.onProgress?.(this._bytesWritten);
|
|
1412
4336
|
this.emit("progress", this._bytesWritten);
|
|
@@ -1414,56 +4338,37 @@ var StreamPlayer = class extends EventEmitter5 {
|
|
|
1414
4338
|
}
|
|
1415
4339
|
/**
|
|
1416
4340
|
* 结束写入
|
|
1417
|
-
*
|
|
4341
|
+
* 通知后端写入完成,但保持播放直到结束
|
|
1418
4342
|
*/
|
|
1419
4343
|
end() {
|
|
1420
|
-
if (this.
|
|
1421
|
-
this.
|
|
1422
|
-
this.writeStream = void 0;
|
|
4344
|
+
if (this.backend) {
|
|
4345
|
+
this.backend.end();
|
|
1423
4346
|
}
|
|
1424
4347
|
}
|
|
1425
4348
|
/**
|
|
1426
4349
|
* 停止播放
|
|
1427
|
-
*
|
|
4350
|
+
* 立即停止播放并释放资源
|
|
1428
4351
|
*/
|
|
1429
4352
|
stop() {
|
|
1430
4353
|
this._stopped = true;
|
|
1431
4354
|
this._started = false;
|
|
1432
4355
|
this._paused = false;
|
|
1433
|
-
if (this.
|
|
1434
|
-
|
|
1435
|
-
this.playerProcess.kill("SIGTERM");
|
|
1436
|
-
} catch (e) {
|
|
1437
|
-
}
|
|
1438
|
-
this.playerProcess = void 0;
|
|
1439
|
-
}
|
|
1440
|
-
if (this.writeStream) {
|
|
1441
|
-
try {
|
|
1442
|
-
this.writeStream.destroy();
|
|
1443
|
-
} catch (e) {
|
|
1444
|
-
}
|
|
1445
|
-
this.writeStream = void 0;
|
|
4356
|
+
if (this.backend) {
|
|
4357
|
+
this.backend.stop();
|
|
1446
4358
|
}
|
|
1447
|
-
this.
|
|
4359
|
+
this._bytesWritten = 0;
|
|
1448
4360
|
this.events?.onStop?.();
|
|
1449
4361
|
this.emit("stop");
|
|
1450
4362
|
}
|
|
1451
4363
|
/**
|
|
1452
4364
|
* 暂停播放
|
|
1453
|
-
* 使用 SIGSTOP 暂停播放器进程
|
|
1454
4365
|
*/
|
|
1455
4366
|
pause() {
|
|
1456
4367
|
if (!this._started || this._paused || this._stopped) {
|
|
1457
4368
|
return;
|
|
1458
4369
|
}
|
|
1459
|
-
if (this.
|
|
1460
|
-
|
|
1461
|
-
this.playerProcess.kill("SIGSTOP");
|
|
1462
|
-
this._paused = true;
|
|
1463
|
-
this.events?.onPause?.();
|
|
1464
|
-
this.emit("pause");
|
|
1465
|
-
} catch (e) {
|
|
1466
|
-
}
|
|
4370
|
+
if (this.backend) {
|
|
4371
|
+
this.backend.pause();
|
|
1467
4372
|
}
|
|
1468
4373
|
}
|
|
1469
4374
|
/**
|
|
@@ -1473,14 +4378,8 @@ var StreamPlayer = class extends EventEmitter5 {
|
|
|
1473
4378
|
if (!this._paused || this._stopped) {
|
|
1474
4379
|
return;
|
|
1475
4380
|
}
|
|
1476
|
-
if (this.
|
|
1477
|
-
|
|
1478
|
-
this.playerProcess.kill("SIGCONT");
|
|
1479
|
-
this._paused = false;
|
|
1480
|
-
this.events?.onResume?.();
|
|
1481
|
-
this.emit("resume");
|
|
1482
|
-
} catch (e) {
|
|
1483
|
-
}
|
|
4381
|
+
if (this.backend) {
|
|
4382
|
+
this.backend.resume();
|
|
1484
4383
|
}
|
|
1485
4384
|
}
|
|
1486
4385
|
/**
|
|
@@ -1507,12 +4406,6 @@ var StreamPlayer = class extends EventEmitter5 {
|
|
|
1507
4406
|
getBytesWritten() {
|
|
1508
4407
|
return this._bytesWritten;
|
|
1509
4408
|
}
|
|
1510
|
-
/**
|
|
1511
|
-
* 获取临时文件路径
|
|
1512
|
-
*/
|
|
1513
|
-
getTempFile() {
|
|
1514
|
-
return this.tempFile;
|
|
1515
|
-
}
|
|
1516
4409
|
/**
|
|
1517
4410
|
* 处理错误
|
|
1518
4411
|
*/
|
|
@@ -1520,20 +4413,6 @@ var StreamPlayer = class extends EventEmitter5 {
|
|
|
1520
4413
|
this.events?.onError?.(error);
|
|
1521
4414
|
this.emit("error", error);
|
|
1522
4415
|
}
|
|
1523
|
-
/**
|
|
1524
|
-
* 删除临时文件
|
|
1525
|
-
*/
|
|
1526
|
-
deleteTempFile() {
|
|
1527
|
-
if (this.tempFile) {
|
|
1528
|
-
try {
|
|
1529
|
-
if (fs2.existsSync(this.tempFile)) {
|
|
1530
|
-
fs2.unlinkSync(this.tempFile);
|
|
1531
|
-
}
|
|
1532
|
-
} catch (e) {
|
|
1533
|
-
}
|
|
1534
|
-
this.tempFile = "";
|
|
1535
|
-
}
|
|
1536
|
-
}
|
|
1537
4416
|
};
|
|
1538
4417
|
|
|
1539
4418
|
// src/index.ts
|
|
@@ -1671,7 +4550,7 @@ function getStreamPlayer() {
|
|
|
1671
4550
|
}
|
|
1672
4551
|
|
|
1673
4552
|
// src/config.ts
|
|
1674
|
-
import * as
|
|
4553
|
+
import * as fs2 from "fs";
|
|
1675
4554
|
import * as path from "path";
|
|
1676
4555
|
import * as os from "os";
|
|
1677
4556
|
var DEFAULT_CONFIG = {
|
|
@@ -1768,21 +4647,21 @@ function resolveEnvValue(value) {
|
|
|
1768
4647
|
}
|
|
1769
4648
|
function loadOrCreateConfig() {
|
|
1770
4649
|
const configDir = path.dirname(CONFIG_PATH);
|
|
1771
|
-
if (!
|
|
4650
|
+
if (!fs2.existsSync(configDir)) {
|
|
1772
4651
|
try {
|
|
1773
|
-
|
|
4652
|
+
fs2.mkdirSync(configDir, { recursive: true });
|
|
1774
4653
|
} catch (err) {
|
|
1775
4654
|
throw new Error(`[ocosay] \u65E0\u6CD5\u521B\u5EFA\u914D\u7F6E\u76EE\u5F55 ${configDir}: ${err}`);
|
|
1776
4655
|
}
|
|
1777
4656
|
}
|
|
1778
|
-
if (!
|
|
4657
|
+
if (!fs2.existsSync(CONFIG_PATH)) {
|
|
1779
4658
|
console.info("[ocosay] \u914D\u7F6E\u6587\u4EF6\u4E0D\u5B58\u5728\uFF0C\u6B63\u5728\u521B\u5EFA\u9ED8\u8BA4\u914D\u7F6E...");
|
|
1780
4659
|
const defaultConfig = generateDefaultConfig();
|
|
1781
4660
|
const configContent = JSON.stringify(defaultConfig, null, 2);
|
|
1782
4661
|
try {
|
|
1783
|
-
|
|
4662
|
+
fs2.writeFileSync(CONFIG_PATH, configContent, "utf-8");
|
|
1784
4663
|
try {
|
|
1785
|
-
|
|
4664
|
+
fs2.chmodSync(CONFIG_PATH, 384);
|
|
1786
4665
|
} catch (err) {
|
|
1787
4666
|
console.warn(`[ocosay] \u65E0\u6CD5\u8BBE\u7F6E\u914D\u7F6E\u6587\u4EF6\u6743\u9650: ${err}`);
|
|
1788
4667
|
}
|
|
@@ -1794,7 +4673,7 @@ function loadOrCreateConfig() {
|
|
|
1794
4673
|
return defaultConfig;
|
|
1795
4674
|
}
|
|
1796
4675
|
try {
|
|
1797
|
-
const content =
|
|
4676
|
+
const content = fs2.readFileSync(CONFIG_PATH, "utf-8");
|
|
1798
4677
|
const stripped = stripComments(content);
|
|
1799
4678
|
const loaded = JSON.parse(stripped);
|
|
1800
4679
|
const merged = mergeWithDefaults(loaded, DEFAULT_CONFIG);
|
|
@@ -1819,11 +4698,14 @@ function loadOrCreateConfig() {
|
|
|
1819
4698
|
|
|
1820
4699
|
// src/plugin.ts
|
|
1821
4700
|
import { readFileSync as readFileSync2 } from "fs";
|
|
1822
|
-
import {
|
|
4701
|
+
import { fileURLToPath } from "url";
|
|
4702
|
+
import { dirname as dirname2, join as join6 } from "path";
|
|
4703
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
4704
|
+
var __dirname = dirname2(__filename);
|
|
1823
4705
|
var pluginName = "ocosay";
|
|
1824
4706
|
var pluginVersion = "0.0.0";
|
|
1825
4707
|
try {
|
|
1826
|
-
const pkg = JSON.parse(readFileSync2(
|
|
4708
|
+
const pkg = JSON.parse(readFileSync2(join6(__dirname, "package.json"), "utf-8"));
|
|
1827
4709
|
pluginVersion = pkg.version || "0.0.0";
|
|
1828
4710
|
} catch {
|
|
1829
4711
|
}
|
|
@@ -1974,11 +4856,11 @@ var OcosayPlugin = async (input, _options) => {
|
|
|
1974
4856
|
if (opencodeShowToast) {
|
|
1975
4857
|
global.__opencode_tui_showToast__ = opencodeShowToast;
|
|
1976
4858
|
}
|
|
1977
|
-
setTimeout(() => {
|
|
4859
|
+
setTimeout(async () => {
|
|
1978
4860
|
if (!opencodeShowToast) return;
|
|
1979
4861
|
if (initError) {
|
|
1980
4862
|
console.info(`[Ocosay] Ocosay v${pluginVersion} Initialization Failed: Initialization failed, please check config`);
|
|
1981
|
-
opencodeShowToast({
|
|
4863
|
+
await opencodeShowToast({
|
|
1982
4864
|
body: {
|
|
1983
4865
|
title: `Ocosay v${pluginVersion} Initialization Failed`,
|
|
1984
4866
|
message: "Initialization failed, please check config",
|
|
@@ -1987,7 +4869,7 @@ var OcosayPlugin = async (input, _options) => {
|
|
|
1987
4869
|
});
|
|
1988
4870
|
} else {
|
|
1989
4871
|
console.info(`[Ocosay] Ocosay v${pluginVersion} Plugin Loaded: Auto-read: ${config.autoRead ? "ON" : "OFF"}`);
|
|
1990
|
-
opencodeShowToast({
|
|
4872
|
+
await opencodeShowToast({
|
|
1991
4873
|
body: {
|
|
1992
4874
|
title: `Ocosay v${pluginVersion} Plugin Loaded`,
|
|
1993
4875
|
message: `Auto-read: ${config.autoRead ? "ON" : "OFF"}`,
|
|
@@ -2045,7 +4927,7 @@ var OcosayPlugin = async (input, _options) => {
|
|
|
2045
4927
|
if (input.client?.tui?.showToast) {
|
|
2046
4928
|
global.__opencode_tui_showToast__ = input.client.tui.showToast;
|
|
2047
4929
|
}
|
|
2048
|
-
setTimeout(() => {
|
|
4930
|
+
setTimeout(async () => {
|
|
2049
4931
|
const showToastFn = input.client?.tui?.showToast;
|
|
2050
4932
|
if (!showToastFn) {
|
|
2051
4933
|
console.warn("[Ocosay] showToast not available");
|
|
@@ -2053,7 +4935,7 @@ var OcosayPlugin = async (input, _options) => {
|
|
|
2053
4935
|
}
|
|
2054
4936
|
if (initError) {
|
|
2055
4937
|
console.info(`[Ocosay] Ocosay v${pluginVersion} Initialization Failed: Initialization failed, please check config`);
|
|
2056
|
-
showToastFn({
|
|
4938
|
+
await showToastFn({
|
|
2057
4939
|
body: {
|
|
2058
4940
|
title: `Ocosay v${pluginVersion} Initialization Failed`,
|
|
2059
4941
|
message: "Initialization failed, please check config",
|
|
@@ -2062,7 +4944,7 @@ var OcosayPlugin = async (input, _options) => {
|
|
|
2062
4944
|
});
|
|
2063
4945
|
} else {
|
|
2064
4946
|
console.info(`[Ocosay] Ocosay v${pluginVersion} Plugin Loaded: Auto-read: ${config.autoRead ? "ON" : "OFF"}`);
|
|
2065
|
-
showToastFn({
|
|
4947
|
+
await showToastFn({
|
|
2066
4948
|
body: {
|
|
2067
4949
|
title: `Ocosay v${pluginVersion} Plugin Loaded`,
|
|
2068
4950
|
message: `Auto-read: ${config.autoRead ? "ON" : "OFF"}`,
|
|
@@ -2083,4 +4965,28 @@ export {
|
|
|
2083
4965
|
plugin_default as default,
|
|
2084
4966
|
server
|
|
2085
4967
|
};
|
|
4968
|
+
/*! Bundled license information:
|
|
4969
|
+
|
|
4970
|
+
howler/dist/howler.js:
|
|
4971
|
+
(*!
|
|
4972
|
+
* howler.js v2.2.4
|
|
4973
|
+
* howlerjs.com
|
|
4974
|
+
*
|
|
4975
|
+
* (c) 2013-2020, James Simpson of GoldFire Studios
|
|
4976
|
+
* goldfirestudios.com
|
|
4977
|
+
*
|
|
4978
|
+
* MIT License
|
|
4979
|
+
*)
|
|
4980
|
+
(*!
|
|
4981
|
+
* Spatial Plugin - Adds support for stereo and 3D audio where Web Audio is supported.
|
|
4982
|
+
*
|
|
4983
|
+
* howler.js v2.2.4
|
|
4984
|
+
* howlerjs.com
|
|
4985
|
+
*
|
|
4986
|
+
* (c) 2013-2020, James Simpson of GoldFire Studios
|
|
4987
|
+
* goldfirestudios.com
|
|
4988
|
+
*
|
|
4989
|
+
* MIT License
|
|
4990
|
+
*)
|
|
4991
|
+
*/
|
|
2086
4992
|
//# sourceMappingURL=plugin.js.map
|