@ohos-ports/dm-howler 2.2.4-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,728 @@
1
+ /**
2
+ * dm-howler Node.js runtime adapter (HarmonyOS / openharmony compatible).
3
+ *
4
+ * Installs a minimal, dependency-free browser-environment shim so that
5
+ * howler.js can load and operate inside a Node.js runtime:
6
+ * - AudioContext : pure-JS Web Audio graph (gain/panner/buffer nodes) with a
7
+ * real RIFF/WAVE decoder for AudioContext.decodeAudioData().
8
+ * - XMLHttpRequest: adapter over Node http/https (remote URLs) and fs (local
9
+ * paths), returning ArrayBuffer responses for audio loading.
10
+ * - Audio : HTMLAudioElement stand-in exposing canPlayType() based on
11
+ * the codecs the adapter can actually decode, so
12
+ * Howler.codecs() reflects real capability.
13
+ * - document/window/navigator: minimal stubs used by howler's unlock logic.
14
+ *
15
+ * This module is side-effect only and safe in browsers (it becomes a no-op when
16
+ * real browser globals are present).
17
+ */
18
+ (function (global) {
19
+ 'use strict';
20
+
21
+ var isNode = typeof process !== 'undefined' && process.versions && process.versions.node;
22
+ if (!isNode) {
23
+ return; // Real browser environment: nothing to do.
24
+ }
25
+
26
+ // Already running in a browser-like embedding with Web Audio support.
27
+ if (typeof global.AudioContext === 'function' &&
28
+ typeof global.Audio === 'function' &&
29
+ typeof global.document !== 'undefined') {
30
+ return;
31
+ }
32
+
33
+ var fs = null, path = null, http = null, https = null;
34
+ try { fs = require('fs'); } catch (e) {}
35
+ try { path = require('path'); } catch (e) {}
36
+ try { http = require('http'); } catch (e) {}
37
+ try { https = require('https'); } catch (e) {}
38
+
39
+ function defineGlobal(name, value) {
40
+ try {
41
+ Object.defineProperty(global, name, {
42
+ value: value, writable: true, configurable: true, enumerable: false
43
+ });
44
+ } catch (e) {
45
+ try { global[name] = value; } catch (e2) { /* ignore */ }
46
+ }
47
+ }
48
+
49
+ /* ------------------------------------------------------------------ *
50
+ * Codecs the adapter can decode (used by Audio.canPlayType).
51
+ * Currently RIFF/WAVE PCM (8/16/24/32-bit int and 32-bit float).
52
+ * ------------------------------------------------------------------ */
53
+ var MIME_SUPPORT = {
54
+ 'audio/wav': 'probably',
55
+ 'audio/wave': 'probably',
56
+ 'audio/x-wav': 'probably',
57
+ 'audio/vnd.wave': 'probably'
58
+ };
59
+ function mimeSupport(mime) {
60
+ if (typeof mime !== 'string') { return ''; }
61
+ var base = String(mime).toLowerCase().replace(/\s*;\s*$/, '');
62
+ if (MIME_SUPPORT[base]) { return MIME_SUPPORT[base]; }
63
+ // Strip codec parameters and retry (e.g. 'audio/wav; codecs="1"').
64
+ var head = base.split(';')[0];
65
+ if (MIME_SUPPORT[head]) { return MIME_SUPPORT[head]; }
66
+ return '';
67
+ }
68
+
69
+ /* ------------------------------------------------------------------ *
70
+ * RIFF/WAVE decoder -> { channels, sampleRate, length, samples[] }
71
+ * ------------------------------------------------------------------ */
72
+ function decodeWav(buffer) {
73
+ var view = new DataView(buffer);
74
+ if (buffer.byteLength < 44) { return null; }
75
+ if (view.getUint8(0) !== 0x52 || view.getUint8(1) !== 0x49 ||
76
+ view.getUint8(2) !== 0x46 || view.getUint8(3) !== 0x46) { return null; } // 'RIFF'
77
+ if (view.getUint8(8) !== 0x57 || view.getUint8(9) !== 0x41 ||
78
+ view.getUint8(10) !== 0x56 || view.getUint8(11) !== 0x45) { return null; } // 'WAVE'
79
+
80
+ var offset = 12, fmt = null, data = null;
81
+ while (offset + 8 <= buffer.byteLength) {
82
+ var id = String.fromCharCode(
83
+ view.getUint8(offset), view.getUint8(offset + 1),
84
+ view.getUint8(offset + 2), view.getUint8(offset + 3));
85
+ var size = view.getUint32(offset + 4, true);
86
+ var body = offset + 8;
87
+ if (id === 'fmt ' && size >= 16) {
88
+ fmt = {
89
+ format: view.getUint16(body, true),
90
+ channels: view.getUint16(body + 2, true),
91
+ sampleRate: view.getUint32(body + 4, true),
92
+ bitsPerSample: view.getUint16(body + 14, true)
93
+ };
94
+ } else if (id === 'data') {
95
+ var avail = buffer.byteLength - body;
96
+ data = new Uint8Array(buffer, body, Math.min(size, avail));
97
+ }
98
+ offset = body + size + (size % 2);
99
+ if (size === 0 || offset <= body) { break; }
100
+ }
101
+ if (!fmt || !data) { return null; }
102
+
103
+ var channels = fmt.channels || 1;
104
+ var bits = fmt.bitsPerSample || 16;
105
+ var bytesPer = bits / 8;
106
+ var frames = Math.floor(data.length / (bytesPer * channels));
107
+ if (frames < 0) { return null; }
108
+ // A header-only WAVE (data chunk of 0 bytes) yields one silent frame so
109
+ // that downstream consumers see a valid, non-zero-length AudioBuffer.
110
+ var silent = (frames === 0);
111
+ if (silent) { frames = 1; }
112
+ var isFloat = fmt.format === 3;
113
+ var samples = [];
114
+ for (var c = 0; c < channels; c++) {
115
+ var arr = new Float32Array(frames);
116
+ samples.push(arr);
117
+ }
118
+ for (var i = 0; i < frames && !silent; i++) {
119
+ for (var ch = 0; ch < channels; ch++) {
120
+ var p = (i * channels + ch) * bytesPer;
121
+ var v = 0;
122
+ if (isFloat && bits === 32) {
123
+ v = view.getFloat32(data.byteOffset + p, true);
124
+ } else if (bits === 8) {
125
+ v = (data[p] - 128) / 128;
126
+ } else if (bits === 16) {
127
+ v = view.getInt16(data.byteOffset + p, true) / 32768;
128
+ } else if (bits === 24) {
129
+ var b0 = data[p], b1 = data[p + 1], b2 = data[p + 2];
130
+ var x = (b2 << 16) | (b1 << 8) | b0;
131
+ if (x & 0x800000) { x |= ~0xFFFFFF; }
132
+ v = x / 8388608;
133
+ } else if (bits === 32) {
134
+ v = view.getInt32(data.byteOffset + p, true) / 2147483648;
135
+ }
136
+ samples[ch][i] = v;
137
+ }
138
+ }
139
+ return { channels: channels, sampleRate: fmt.sampleRate, length: frames, samples: samples };
140
+ }
141
+
142
+ /* ------------------------------------------------------------------ *
143
+ * Web Audio: AudioParam
144
+ * ------------------------------------------------------------------ */
145
+ function AudioParam(value) {
146
+ this.value = value;
147
+ this.defaultValue = value;
148
+ this.minValue = -3.4028234663852886e38;
149
+ this.maxValue = 3.4028234663852886e38;
150
+ }
151
+ AudioParam.prototype.setValueAtTime = function (v) { this.value = v; return this; };
152
+ AudioParam.prototype.linearRampToValueAtTime = function (v) { this.value = v; return this; };
153
+ AudioParam.prototype.exponentialRampToValueAtTime = function (v) { this.value = v; return this; };
154
+ AudioParam.prototype.setTargetAtTime = function (v) { this.value = v; return this; };
155
+ AudioParam.prototype.setValueCurveAtTime = function () { return this; };
156
+ AudioParam.prototype.cancelScheduledValues = function () { return this; };
157
+ AudioParam.prototype.cancelAndHoldAtTime = function () { return this; };
158
+
159
+ /* ------------------------------------------------------------------ *
160
+ * Web Audio: base node + concrete node types
161
+ * ------------------------------------------------------------------ */
162
+ function AudioNode(ctx) {
163
+ this.context = ctx;
164
+ this._connected = [];
165
+ this.numberOfInputs = 1;
166
+ this.numberOfOutputs = 1;
167
+ this.channelCount = 2;
168
+ }
169
+ AudioNode.prototype.connect = function (dest) {
170
+ if (dest && this._connected.indexOf(dest) === -1) { this._connected.push(dest); }
171
+ return dest;
172
+ };
173
+ AudioNode.prototype.disconnect = function () { this._connected.length = 0; };
174
+
175
+ function GainNode(ctx) {
176
+ AudioNode.call(this, ctx);
177
+ this.gain = new AudioParam(1);
178
+ }
179
+ GainNode.prototype = Object.create(AudioNode.prototype);
180
+
181
+ function PannerNode(ctx) {
182
+ AudioNode.call(this, ctx);
183
+ this.panningModel = 'HRTF';
184
+ this.distanceModel = 'inverse';
185
+ this.refDistance = 1;
186
+ this.maxDistance = 10000;
187
+ this.rolloffFactor = 1;
188
+ this.coneInnerAngle = 360;
189
+ this.coneOuterAngle = 360;
190
+ this.coneOuterGain = 0;
191
+ this.positionX = new AudioParam(0);
192
+ this.positionY = new AudioParam(0);
193
+ this.positionZ = new AudioParam(0);
194
+ }
195
+ PannerNode.prototype = Object.create(AudioNode.prototype);
196
+ PannerNode.prototype.setPosition = function (x, y, z) {
197
+ this.positionX.value = x; this.positionY.value = y; this.positionZ.value = z;
198
+ };
199
+ PannerNode.prototype.setOrientation = function () {};
200
+ PannerNode.prototype.orient = function (x, y, z, x2, y2, z2) {
201
+ if (typeof x === 'number') {
202
+ this.setPosition(x, y, z);
203
+ if (typeof x2 === 'number') { this.setOrientation(x2, y2, z2); }
204
+ }
205
+ };
206
+
207
+ function StereoPannerNode(ctx) {
208
+ AudioNode.call(this, ctx);
209
+ this.pan = new AudioParam(0);
210
+ }
211
+ StereoPannerNode.prototype = Object.create(AudioNode.prototype);
212
+
213
+ function AudioBufferSourceNode(ctx) {
214
+ AudioNode.call(this, ctx);
215
+ this.buffer = null;
216
+ this.loop = false;
217
+ this.loopStart = 0;
218
+ this.loopEnd = 0;
219
+ this.playbackRate = new AudioParam(1);
220
+ this.onended = null;
221
+ }
222
+ AudioBufferSourceNode.prototype = Object.create(AudioNode.prototype);
223
+ AudioBufferSourceNode.prototype.start = function (when, offset, duration) {
224
+ var self = this;
225
+ // Real audio output is not rendered in the Node adapter; the playback
226
+ // timeline is simulated so that duration/onended semantics stay correct.
227
+ this._startedAt = (typeof when === 'number' ? when : 0);
228
+ this._offset = (typeof offset === 'number' ? offset : 0);
229
+ var dur = 0;
230
+ if (this.buffer) {
231
+ dur = (typeof duration === 'number' ? duration : this.buffer.duration - this._offset);
232
+ if (this.loop) { dur = Infinity; }
233
+ }
234
+ if (typeof this.onplay === 'function') { this.onplay(); }
235
+ if (typeof this.onended === 'function' && isFinite(dur)) {
236
+ this._endedTimer = setTimeout(function () {
237
+ if (typeof self.onended === 'function') { self.onended(); }
238
+ }, Math.max(0, dur * 1000));
239
+ }
240
+ };
241
+ AudioBufferSourceNode.prototype.stop = function () {
242
+ if (this._endedTimer) { clearTimeout(this._endedTimer); this._endedTimer = null; }
243
+ if (typeof this.onended === 'function') { this.onended(); }
244
+ };
245
+ AudioBufferSourceNode.prototype.noteOn = function (when) { this.start(when); };
246
+ AudioBufferSourceNode.prototype.noteOff = function () { this.stop(); };
247
+
248
+ function AudioBuffer(ctx, channels, length, sampleRate) {
249
+ this.context = ctx;
250
+ this.numberOfChannels = channels;
251
+ this.length = length;
252
+ this.sampleRate = sampleRate;
253
+ this.duration = length / sampleRate;
254
+ this._samples = [];
255
+ for (var c = 0; c < channels; c++) { this._samples.push(new Float32Array(length)); }
256
+ }
257
+ AudioBuffer.prototype.getChannelData = function (channel) {
258
+ return this._samples[channel] || new Float32Array(this.length);
259
+ };
260
+ AudioBuffer.prototype.copyFromChannel = function (dest, ch) {
261
+ var src = this.getChannelData(ch);
262
+ dest.set(src.subarray(0, Math.min(dest.length, src.length)));
263
+ };
264
+ AudioBuffer.prototype.copyToChannel = function (src, ch) {
265
+ var dst = this.getChannelData(ch);
266
+ dst.set(src.subarray(0, Math.min(dst.length, src.length)));
267
+ };
268
+
269
+ function AudioListener(ctx) {
270
+ this.positionX = new AudioParam(0);
271
+ this.positionY = new AudioParam(0);
272
+ this.positionZ = new AudioParam(0);
273
+ this.forwardX = new AudioParam(0);
274
+ this.forwardY = new AudioParam(0);
275
+ this.forwardZ = new AudioParam(-1);
276
+ this.upX = new AudioParam(0);
277
+ this.upY = new AudioParam(1);
278
+ this.upZ = new AudioParam(0);
279
+ }
280
+ AudioListener.prototype.setPosition = function () {};
281
+ AudioListener.prototype.setOrientation = function () {};
282
+
283
+ function AudioDestinationNode(ctx) {
284
+ AudioNode.call(this, ctx);
285
+ this.maxChannelCount = 2;
286
+ }
287
+
288
+ /* ------------------------------------------------------------------ *
289
+ * AudioContext (pure JS)
290
+ * ------------------------------------------------------------------ */
291
+ function AudioContext() {
292
+ this.sampleRate = 44100;
293
+ this.state = 'running';
294
+ this.destination = new AudioDestinationNode(this);
295
+ this.listener = new AudioListener(this);
296
+ this._epoch = Date.now();
297
+ // Live playback clock: currentTime advances in real time.
298
+ var self = this;
299
+ Object.defineProperty(this, 'currentTime', {
300
+ get: function () { return (Date.now() - self._epoch) / 1000; },
301
+ configurable: true
302
+ });
303
+ }
304
+ AudioContext.prototype._tick = function () {
305
+ return (Date.now() - this._epoch) / 1000;
306
+ };
307
+ AudioContext.prototype.createGain = function () { return new GainNode(this); };
308
+ AudioContext.prototype.createGainNode = function () { return this.createGain(); };
309
+ AudioContext.prototype.createPanner = function () { return new PannerNode(this); };
310
+ AudioContext.prototype.createStereoPanner = function () { return new StereoPannerNode(this); };
311
+ AudioContext.prototype.createBufferSource = function () { return new AudioBufferSourceNode(this); };
312
+ AudioContext.prototype.createBuffer = function (channels, length, sampleRate) {
313
+ return new AudioBuffer(this, channels, length, sampleRate);
314
+ };
315
+ AudioContext.prototype.createScriptProcessor = function (bufferSize) {
316
+ var node = new AudioNode(this);
317
+ node.bufferSize = bufferSize || 4096;
318
+ node.onaudioprocess = null;
319
+ return node;
320
+ };
321
+ AudioContext.prototype.createDynamicsCompressor = function () {
322
+ var node = new GainNode(this);
323
+ node.threshold = new AudioParam(-24);
324
+ node.knee = new AudioParam(30);
325
+ node.ratio = new AudioParam(12);
326
+ node.attack = new AudioParam(0.003);
327
+ node.release = new AudioParam(0.25);
328
+ return node;
329
+ };
330
+ AudioContext.prototype.createBiquadFilter = function () {
331
+ var node = new GainNode(this);
332
+ node.type = 'lowpass';
333
+ node.frequency = new AudioParam(350);
334
+ node.Q = new AudioParam(1);
335
+ node.detune = new AudioParam(0);
336
+ node.gain = new AudioParam(0);
337
+ return node;
338
+ };
339
+ AudioContext.prototype.createOscillator = function () {
340
+ var node = new AudioNode(this);
341
+ node.type = 'sine';
342
+ node.frequency = new AudioParam(440);
343
+ node.detune = new AudioParam(0);
344
+ node.start = function () {};
345
+ node.stop = function () {};
346
+ return node;
347
+ };
348
+
349
+ /**
350
+ * Decode audio data. Accepts Node Buffer / ArrayBuffer / TypedArray.
351
+ * Supports RIFF/WAVE PCM; rejects anything the decoder cannot parse.
352
+ */
353
+ AudioContext.prototype.decodeAudioData = function (audioData, successCallback, errorCallback) {
354
+ var ctx = this;
355
+ var promise = new Promise(function (resolve, reject) {
356
+ var bytes = audioData;
357
+ try {
358
+ if (bytes && typeof Buffer !== 'undefined' && Buffer.isBuffer(bytes)) {
359
+ bytes = new Uint8Array(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength));
360
+ } else if (bytes && bytes.buffer instanceof ArrayBuffer && !(bytes instanceof Uint8Array)) {
361
+ bytes = new Uint8Array(bytes.buffer.slice(0));
362
+ } else if (!(bytes instanceof Uint8Array)) {
363
+ bytes = new Uint8Array(bytes);
364
+ }
365
+ // Copy into a standalone ArrayBuffer (DataView alignment safety).
366
+ var ab = new ArrayBuffer(bytes.length);
367
+ new Uint8Array(ab).set(bytes);
368
+ var decoded = decodeWav(ab);
369
+ if (!decoded) { throw new Error('Unsupported audio format (RIFF/WAVE PCM supported)'); }
370
+ var buffer = new AudioBuffer(
371
+ ctx, decoded.channels, decoded.length, decoded.sampleRate);
372
+ for (var c = 0; c < decoded.channels; c++) {
373
+ buffer.getChannelData(c).set(decoded.samples[c]);
374
+ }
375
+ resolve(buffer);
376
+ } catch (err) {
377
+ reject(err);
378
+ }
379
+ });
380
+ if (typeof successCallback === 'function') {
381
+ promise.then(successCallback, errorCallback || function () {});
382
+ }
383
+ return promise;
384
+ };
385
+ AudioContext.prototype.suspend = function () { this.state = 'suspended'; return Promise.resolve(); };
386
+ AudioContext.prototype.resume = function () { this.state = 'running'; return Promise.resolve(); };
387
+ AudioContext.prototype.close = function () { this.state = 'closed'; return Promise.resolve(); };
388
+
389
+ /* ------------------------------------------------------------------ *
390
+ * XMLHttpRequest over Node http/https/fs
391
+ * ------------------------------------------------------------------ */
392
+ function XMLHttpRequest() {
393
+ this.readyState = 0;
394
+ this.status = 0;
395
+ this.statusText = '';
396
+ this.response = null;
397
+ this.responseText = '';
398
+ this.responseType = '';
399
+ this.withCredentials = false;
400
+ this.onreadystatechange = null;
401
+ this.onload = null;
402
+ this.onerror = null;
403
+ this.onabort = null;
404
+ this.onprogress = null;
405
+ this._listeners = {};
406
+ this._headers = {};
407
+ this._method = 'GET';
408
+ this._url = '';
409
+ }
410
+ XMLHttpRequest.UNSENT = 0;
411
+ XMLHttpRequest.OPENED = 1;
412
+ XMLHttpRequest.HEADERS_RECEIVED = 2;
413
+ XMLHttpRequest.LOADING = 3;
414
+ XMLHttpRequest.DONE = 4;
415
+ XMLHttpRequest.prototype._fire = function (type, event) {
416
+ var handler = this['on' + type];
417
+ if (typeof handler === 'function') { handler.call(this, event); }
418
+ var list = this._listeners[type];
419
+ if (list) {
420
+ for (var i = 0; i < list.length; i++) { list[i].call(this, event); }
421
+ }
422
+ };
423
+ XMLHttpRequest.prototype._setReadyState = function (state) {
424
+ this.readyState = state;
425
+ this._fire('readystatechange', null);
426
+ };
427
+ XMLHttpRequest.prototype.open = function (method, url) {
428
+ this._method = String(method || 'GET').toUpperCase();
429
+ this._url = String(url || '');
430
+ this.status = 0;
431
+ this.response = null;
432
+ this.responseText = '';
433
+ this._setReadyState(1);
434
+ };
435
+ XMLHttpRequest.prototype.setRequestHeader = function (name, value) {
436
+ this._headers[String(name)] = String(value);
437
+ };
438
+ XMLHttpRequest.prototype.getResponseHeader = function () { return null; };
439
+ XMLHttpRequest.prototype.getAllResponseHeaders = function () { return ''; };
440
+ XMLHttpRequest.prototype.abort = function () {
441
+ if (this._req && typeof this._req.abort === 'function') { this._req.abort(); }
442
+ this._setReadyState(4);
443
+ this._fire('abort', null);
444
+ };
445
+ XMLHttpRequest.prototype.addEventListener = function (type, fn) {
446
+ (this._listeners[type] = this._listeners[type] || []).push(fn);
447
+ };
448
+ XMLHttpRequest.prototype.removeEventListener = function (type, fn) {
449
+ var list = this._listeners[type] || [];
450
+ var idx = list.indexOf(fn);
451
+ if (idx !== -1) { list.splice(idx, 1); }
452
+ };
453
+ XMLHttpRequest.prototype._finish = function (data) {
454
+ this._setReadyState(2);
455
+ this._setReadyState(3);
456
+ if (this.responseType === 'arraybuffer' || this.responseType === 'blob') {
457
+ this.response = data;
458
+ } else {
459
+ this.responseText = Buffer.from(data).toString('utf8');
460
+ this.response = this.responseText;
461
+ }
462
+ this._setReadyState(4);
463
+ this._fire('load', null);
464
+ };
465
+ XMLHttpRequest.prototype._fail = function (err) {
466
+ this._setReadyState(4);
467
+ this._fire('error', err);
468
+ };
469
+ XMLHttpRequest.prototype.send = function () {
470
+ var self = this;
471
+ var url = this._url;
472
+
473
+ // Local file paths (absolute, relative or file://) -> fs adapter.
474
+ if (!/^https?:\/\//i.test(url)) {
475
+ if (!fs) { setImmediate(function () { self._fail(new Error('fs unavailable')); }); return; }
476
+ var file = url;
477
+ if (/^file:\/\//i.test(file)) {
478
+ try { file = require('url').fileURLToPath(file); } catch (e) {}
479
+ }
480
+ try {
481
+ file = path ? path.resolve(process.cwd(), file) : file;
482
+ } catch (e) {}
483
+ fs.readFile(file, function (err, buf) {
484
+ if (err) { self._fail(err); return; }
485
+ self.status = 200;
486
+ self.statusText = 'OK';
487
+ self._finish(buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength));
488
+ });
489
+ return;
490
+ }
491
+
492
+ if (!http || !https) { setImmediate(function () { self._fail(new Error('http unavailable')); }); return; }
493
+ var mod = /^https:\/\//i.test(url) ? https : http;
494
+ try {
495
+ var parsed = new (require('url').URL)(url);
496
+ var req = mod.request({
497
+ hostname: parsed.hostname,
498
+ port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
499
+ path: parsed.pathname + parsed.search,
500
+ method: this._method,
501
+ headers: this._headers
502
+ }, function (res) {
503
+ self.status = res.statusCode;
504
+ self.statusText = res.statusMessage || '';
505
+ var chunks = [];
506
+ res.on('data', function (c) { chunks.push(c); });
507
+ res.on('end', function () {
508
+ var buf = Buffer.concat(chunks);
509
+ self._finish(buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength));
510
+ });
511
+ });
512
+ req.on('error', function (err) { self._fail(err); });
513
+ req.end();
514
+ this._req = req;
515
+ } catch (err) {
516
+ setImmediate(function () { self._fail(err); });
517
+ }
518
+ };
519
+
520
+ /* ------------------------------------------------------------------ *
521
+ * HTMLAudioElement stand-in
522
+ * ------------------------------------------------------------------ */
523
+ function MediaError() { this.code = 4; this.message = ''; }
524
+ function Audio(src) {
525
+ this._src = src || '';
526
+ this._listeners = {};
527
+ this._duration = 0;
528
+ this.currentTime = 0;
529
+ this.volume = 1;
530
+ this.muted = false;
531
+ this.paused = true;
532
+ this.loop = false;
533
+ this.playbackRate = 1;
534
+ this.readyState = 0; // HAVE_NOTHING until load completes
535
+ this.networkState = 0;
536
+ this.error = null;
537
+ this._unlocked = false;
538
+ this.preload = 'auto';
539
+ this.controls = false;
540
+ this.autoplay = false;
541
+ if (src) { this._scheduleLoad(); }
542
+ }
543
+ Audio.HAVE_NOTHING = 0;
544
+ Audio.HAVE_METADATA = 1;
545
+ Audio.HAVE_CURRENT_DATA = 2;
546
+ Audio.HAVE_FUTURE_DATA = 3;
547
+ Audio.HAVE_ENOUGH_DATA = 4;
548
+ Audio.prototype.canPlayType = function (mime) { return mimeSupport(mime); };
549
+ Audio.prototype._fire = function (type) {
550
+ var self = this;
551
+ var handler = this['on' + type];
552
+ if (typeof handler === 'function') { handler.call(this, { type: type, target: this }); }
553
+ var list = this._listeners[type];
554
+ if (list) {
555
+ for (var i = 0; i < list.length; i++) { list[i].call(this, { type: type, target: self }); }
556
+ }
557
+ };
558
+ Audio.prototype.addEventListener = function (type, fn) {
559
+ (this._listeners[type] = this._listeners[type] || []).push(fn);
560
+ };
561
+ Audio.prototype.removeEventListener = function (type, fn) {
562
+ var list = this._listeners[type] || [];
563
+ var idx = list.indexOf(fn);
564
+ if (idx !== -1) { list.splice(idx, 1); }
565
+ };
566
+ Audio.prototype.dispatchEvent = function (evt) {
567
+ if (evt && evt.type) { this._fire(evt.type); }
568
+ return true;
569
+ };
570
+ Audio.prototype._scheduleLoad = function () {
571
+ var self = this;
572
+ // Read the real duration for local WAVE files when possible; other
573
+ // formats stay at duration 0 (the Web Audio backend handles decoding).
574
+ setImmediate(function () {
575
+ self.readyState = 1;
576
+ self._fire('loadstart');
577
+ var dur = 0;
578
+ try {
579
+ if (fs && self._src && !/^https?:\/\//i.test(self._src)) {
580
+ var file = path ? path.resolve(process.cwd(), self._src) : self._src;
581
+ var stat = fs.statSync(file);
582
+ var fd = fs.openSync(file, 'r');
583
+ var head = Buffer.alloc(4096);
584
+ var read = fs.readSync(fd, head, 0, 4096, 0);
585
+ fs.closeSync(fd);
586
+ var wav = decodeWav(head.buffer.slice(head.byteOffset, head.byteOffset + read));
587
+ if (wav && wav.sampleRate && wav.length) {
588
+ var bytesPerFrame = wav.channels * 2;
589
+ var dataBytes = Math.max(0, stat.size - 44);
590
+ dur = dataBytes / (bytesPerFrame * wav.sampleRate);
591
+ }
592
+ }
593
+ } catch (e) { /* probing failures keep duration 0 */ }
594
+ self._duration = dur;
595
+ self.readyState = 4;
596
+ self.networkState = 1;
597
+ self._fire('loadedmetadata');
598
+ self._fire('loadeddata');
599
+ self._fire('canplay');
600
+ self._fire('canplaythrough');
601
+ });
602
+ };
603
+ Object.defineProperty(Audio.prototype, 'duration', {
604
+ get: function () { return this._duration; }
605
+ });
606
+ Object.defineProperty(Audio.prototype, 'src', {
607
+ get: function () { return this._src; },
608
+ set: function (v) {
609
+ this._src = String(v || '');
610
+ this.readyState = 0;
611
+ if (this._src) { this._scheduleLoad(); }
612
+ }
613
+ });
614
+ Audio.prototype.load = function () {
615
+ this.readyState = 0;
616
+ if (this._src) { this._scheduleLoad(); }
617
+ };
618
+ Audio.prototype.play = function () {
619
+ this.paused = false;
620
+ var self = this;
621
+ setImmediate(function () {
622
+ self._fire('play');
623
+ self._fire('playing');
624
+ });
625
+ return Promise.resolve();
626
+ };
627
+ Audio.prototype.pause = function () {
628
+ this.paused = true;
629
+ this._fire('pause');
630
+ };
631
+ Audio.prototype.destroy = function () {};
632
+
633
+ /* ------------------------------------------------------------------ *
634
+ * Minimal document / navigator / window
635
+ * ------------------------------------------------------------------ */
636
+ var documentShim = {
637
+ readyState: 'complete',
638
+ hidden: false,
639
+ visibilityState: 'visible',
640
+ createElement: function (tag) {
641
+ var t = String(tag || '').toLowerCase();
642
+ if (t === 'audio') { return new Audio(); }
643
+ if (t === 'canvas') {
644
+ return {
645
+ getContext: function () { return null; },
646
+ width: 0, height: 0
647
+ };
648
+ }
649
+ return { style: {}, addEventListener: function () {}, removeEventListener: function () {} };
650
+ },
651
+ createElementNS: function (ns, tag) { return documentShim.createElement(tag); },
652
+ createTextNode: function (t) { return { nodeValue: t }; },
653
+ addEventListener: function () {}, // no user-gesture unlock needed on Node
654
+ removeEventListener: function () {},
655
+ getElementById: function () { return null; },
656
+ querySelector: function () { return null; },
657
+ querySelectorAll: function () { return []; },
658
+ documentElement: { style: {} },
659
+ body: { appendChild: function () {}, style: {} },
660
+ head: { appendChild: function () {}, style: {} }
661
+ };
662
+
663
+ defineGlobal('AudioContext', AudioContext);
664
+ defineGlobal('webkitAudioContext', AudioContext);
665
+ defineGlobal('OfflineAudioContext', AudioContext);
666
+ defineGlobal('Audio', Audio);
667
+ defineGlobal('XMLHttpRequest', XMLHttpRequest);
668
+ defineGlobal('document', documentShim);
669
+ defineGlobal('MediaError', MediaError);
670
+
671
+ // Minimal location: howler checks window.location.protocol when building URLs.
672
+ var locationShim = {
673
+ href: 'file://' + (function () { try { return process.cwd(); } catch (e) { return '/'; } })() + '/',
674
+ protocol: 'file:',
675
+ host: '',
676
+ hostname: '',
677
+ port: '',
678
+ origin: 'null',
679
+ pathname: '/',
680
+ search: '',
681
+ hash: ''
682
+ };
683
+ defineGlobal('location', locationShim);
684
+ if (global.window) { try { global.window.location = locationShim; } catch (e) {} }
685
+
686
+ // window points at the global object so `window.navigator` etc. resolve.
687
+ if (typeof global.window === 'undefined') {
688
+ defineGlobal('window', global);
689
+ }
690
+ // Make sure window also sees the shims when window !== global.
691
+ if (global.window && global.window !== global) {
692
+ ['AudioContext', 'webkitAudioContext', 'Audio', 'XMLHttpRequest', 'document']
693
+ .forEach(function (k) {
694
+ if (typeof global.window[k] === 'undefined') { global.window[k] = global[k]; }
695
+ });
696
+ }
697
+
698
+ // Node's built-in navigator lacks browser fields howler reads
699
+ // (appVersion/platform/userAgent). Ensure they exist.
700
+ var NAV_BASE = {
701
+ appVersion: '5.0 (Node.js; ' + (process.platform || 'ohos') + ') NodeAdapter',
702
+ platform: process.platform === 'openharmony' ? 'OpenHarmony' : (process.platform || 'node'),
703
+ userAgent: 'Node.js/' + (process.versions && process.versions.node ? process.versions.node : 'unknown'),
704
+ language: 'en-US',
705
+ languages: ['en-US'],
706
+ vendor: '',
707
+ maxTouchPoints: 0
708
+ };
709
+ if (typeof global.navigator === 'object' && global.navigator !== null) {
710
+ Object.keys(NAV_BASE).forEach(function (k) {
711
+ if (typeof global.navigator[k] === 'undefined') {
712
+ try { global.navigator[k] = NAV_BASE[k]; } catch (e) { /* read-only */ }
713
+ }
714
+ });
715
+ } else {
716
+ defineGlobal('navigator', Object.create(null, (function () {
717
+ var desc = {};
718
+ Object.keys(NAV_BASE).forEach(function (k) {
719
+ desc[k] = { value: NAV_BASE[k], writable: true, configurable: true, enumerable: true };
720
+ });
721
+ return desc;
722
+ })()));
723
+ }
724
+
725
+ // Mark that the adapter is active (used for diagnostics).
726
+ defineGlobal('__DM_HOWLER_NODE_ADAPTER__', true);
727
+
728
+ })(typeof globalThis !== 'undefined' ? globalThis : (typeof self !== 'undefined' ? self : this));