1163 lines
30 KiB
JavaScript
Executable File

(function (root, factory) {
if (typeof exports === 'object' && typeof module !== 'undefined') {
var exports_obj = factory();
module.exports = exports_obj;
} else if (typeof define === 'function' && define.amd) {
define([], factory);
} else {
var exports_obj = factory();
root.Howler = exports_obj.Howler;
root.Howl = exports_obj.Howl;
}
}(typeof self !== 'undefined' ? self : this, function () {
/**
* based on Howler.js - (c) 2013-2020, James Simpson of GoldFire Studios
* Mostly compatible API
*/
class HowlerGlobal {
constructor() {
this._counter = 1000;
this._howls = [];
this._volume = 1;
this._muted = false;
this._html5Pool = [];
this._codecs = {};
this.ctx = null;
this.masterGain = null;
this.usingWebAudio = true;
this.autoUnlock = true;
this._audioUnlocked = false;
this._environment = null;
this._setup();
}
volume(vol) {
if (vol === undefined) return this._volume;
this._volume = Math.max(0, Math.min(1, vol));
if (!this._muted) {
if (this.usingWebAudio && this.masterGain) {
this.masterGain.gain.setValueAtTime(this._volume, this.ctx.currentTime);
}
this._howls.forEach(howl => howl._updateVolume());
}
return this;
}
mute(muted = true) {
this._muted = muted;
let vol = muted ? 0 : this._volume;
if (this.usingWebAudio && this.masterGain) {
this.masterGain.gain.setValueAtTime(vol, this.ctx.currentTime);
}
this._howls.forEach(howl => howl._updateVolume());
return this;
}
stop() {
this._howls.forEach(howl => howl.stop());
return this;
}
unload() {
this._howls.slice().forEach(howl => howl.unload());
if (this.ctx?.close) {
this.ctx.close();
this.ctx = null;
this._setupAudioContext();
}
return this;
}
codecs(ext) {
return this._codecs[ext?.replace(/^x-/, '')];
}
_setup() {
this._setupAudioContext();
this._setupCodecs();
if (this.autoUnlock) this._setupUnlock();
}
_setupAudioContext() {
if (!this.usingWebAudio) return;
try {
this.ctx = new (window.AudioContext || window.webkitAudioContext)();
this.masterGain = this.ctx.createGain();
this.masterGain.connect(this.ctx.destination);
this.masterGain.gain.setValueAtTime(this._muted ? 0 : this._volume, this.ctx.currentTime);
this.listener = {
position: { x: 0, y: 0, z: 0 },
orientation: {
forward: { x: 0, y: 0, z: -1 },
up: { x: 0, y: 1, z: 0 }
},
velocity: { x: 0, y: 0, z: 0 },
update: () => this._updateListener()
};
} catch (e) {
this.usingWebAudio = false;
}
}
_setupCodecs() {
if (typeof Audio === 'undefined') return;
let audio = new Audio();
let test = (mime) => !!audio.canPlayType(mime).replace(/^no$/, '');
this._codecs = {
mp3: test('audio/mpeg;'),
opus: test('audio/ogg; codecs="opus"'),
ogg: test('audio/ogg; codecs="vorbis"'),
wav: test('audio/wav; codecs="1"') || test('audio/wav'),
aac: test('audio/aac;'),
m4a: test('audio/x-m4a;') || test('audio/m4a;') || test('audio/aac;'),
mp4: test('audio/x-mp4;') || test('audio/mp4;') || test('audio/aac;'),
webm: test('audio/webm; codecs="vorbis"'),
flac: test('audio/x-flac;') || test('audio/flac;')
};
}
_setupUnlock() {
if (this._audioUnlocked || !this.ctx) return;
let unlock = () => {
if (this._audioUnlocked) return;
let buffer = this.ctx.createBuffer(1, 1, 22050);
let source = this.ctx.createBufferSource();
source.buffer = buffer;
source.connect(this.ctx.destination);
source.start(0);
if (this.ctx.resume) this.ctx.resume();
source.onended = () => {
this._audioUnlocked = true;
document.removeEventListener('touchstart', unlock, true);
document.removeEventListener('click', unlock, true);
this._howls.forEach(howl => howl._emit('unlock'));
};
};
document.addEventListener('touchstart', unlock, true);
document.addEventListener('click', unlock, true);
}
_getHtml5Audio() {
return this._html5Pool.pop() || new Audio();
}
_releaseHtml5Audio(audio) {
if (audio._unlocked && this._html5Pool.length < 10) {
this._html5Pool.push(audio);
}
}
environment(env) {
if(!env)
return this._environment;
if (typeof env === 'string') {
let presets = {
room: { size: 'small', decay: 1.5, dampening: 0.8 },
hall: { size: 'medium', decay: 2.5, dampening: 0.6 },
cathedral: { size: 'large', decay: 8.0, dampening: 0.1 },
cave: { size: 'large', decay: 6.0, dampening: 0.2 },
underwater: { size: 'medium', decay: 3.0, dampening: 0.9 }
};
this._environment = presets[env] || null;
} else {
this._environment = env;
}
this._howls.forEach(howl => howl._updateEnvironment?.());
return this;
}
_updateListener() {
if (!this.ctx?.listener) return;
let { position, orientation, velocity } = this.listener;
let l = this.ctx.listener;
if (l.positionX) {
l.positionX.setValueAtTime(position.x, this.ctx.currentTime);
l.positionY.setValueAtTime(position.y, this.ctx.currentTime);
l.positionZ.setValueAtTime(position.z, this.ctx.currentTime);
l.forwardX.setValueAtTime(orientation.forward.x, this.ctx.currentTime);
l.forwardY.setValueAtTime(orientation.forward.y, this.ctx.currentTime);
l.forwardZ.setValueAtTime(orientation.forward.z, this.ctx.currentTime);
l.upX.setValueAtTime(orientation.up.x, this.ctx.currentTime);
l.upY.setValueAtTime(orientation.up.y, this.ctx.currentTime);
l.upZ.setValueAtTime(orientation.up.z, this.ctx.currentTime);
} else {
l.setPosition(position.x, position.y, position.z);
l.setOrientation(
orientation.forward.x, orientation.forward.y, orientation.forward.z,
orientation.up.x, orientation.up.y, orientation.up.z
);
}
if (l.setVelocity) {
l.setVelocity(velocity.x, velocity.y, velocity.z);
}
}
}
class Howl {
constructor(options = {}) {
if(options.url) options.src = options.url; // backwards compat
if(options.urls) options.src = options.urls; // backwards compat
if (!options.src?.length) throw new Error('src required');
Object.assign(this, {
_src: Array.isArray(options.src) ? options.src : [options.src],
_volume: options.volume ?? 1,
_rate: options.rate ?? 1,
_loop: options.loop ?? false,
_muted: options.mute ?? false,
_preload: options.preload ?? true,
_autoplay: options.autoplay ?? false,
_sprite: options.sprite ?? {},
_format: options.format ? (Array.isArray(options.format) ? options.format : [options.format]) : null,
_html5: options.html5 ?? false,
_pool: options.pool ?? 5,
_xhr: { method: 'GET', ...options.xhr },
_duration: 0,
_state: 'unloaded',
_sounds: [],
_queue: [],
_listeners: {},
_webAudio: Howler.usingWebAudio && !options.html5,
_analyzer: null,
_frequencyCallback: null,
_frequencyData: null,
_frequencyBands: options.frequencyBands || 16,
_frequencyBoost: 4.0,
_frequencyEnabled: false,
_effects: []
});
if (options.spatial) {
this.spatial = {
enabled: true,
position: { x: 0, y: 0, z: 0 },
orientation: { x: 0, y: 0, z: -1 },
velocity: { x: 0, y: 0, z: 0 },
distance: {
ref: 1,
max: 10000,
rolloff: 1,
model: 'inverse'
},
cone: {
inner: 360,
outer: 360,
outerGain: 0
},
occlusion: 0,
obstruction: 0,
panningModel: 'HRTF',
update: () => this._updateSpatial()
};
if (Array.isArray(options.spatial.position)) {
let [x, y, z] = options.spatial.position;
Object.assign(this.spatial.position, { x, y, z });
}
if (options.spatial.distance) {
Object.assign(this.spatial.distance, options.spatial.distance);
}
if (options.spatial.cone) {
Object.assign(this.spatial.cone, options.spatial.cone);
}
if (options.spatial.panningModel) {
this.spatial.panningModel = options.spatial.panningModel;
}
}
['load', 'loaderror', 'play', 'pause', 'stop', 'end', 'fade', 'volume', 'rate', 'seek', 'mute', 'unlock'].forEach(event => {
if (options[`on${event}`]) this.on(event, options[`on${event}`]);
});
Howler._howls.push(this);
if (this._autoplay) this._queue.push({ event: 'play', action: () => this.play() });
if (this._preload) this.load();
}
load() {
if (this._state !== 'unloaded') return Promise.resolve(this);
this._state = 'loading';
return new Promise((resolve, reject) => {
let url = this._selectSource();
if (!url) {
let error = 'No compatible source found';
this._emit('loaderror', null, error);
return reject(new Error(error));
}
this._src = url;
if (this._webAudio) {
this._loadWebAudio(url).then(resolve).catch(reject);
} else {
this._loadHtml5(url).then(resolve).catch(reject);
}
});
}
play(sprite) {
if (typeof sprite === 'number') {
let sound = this._sounds.find(s => s._id === sprite);
return sound ? this._playSound(sound) : null;
}
if (this._state !== 'loaded') {
if (this._state === 'loading') {
return new Promise(resolve => {
this._queue.push({ event: 'play', action: () => resolve(this.play(sprite)) });
});
}
return this.load().then(() => this.play(sprite));
}
let sound = this._getSound();
sound._sprite = sprite || '__default';
return this._playSound(sound);
}
pause(id) {
this._getSounds(id).forEach(sound => {
if (!sound._paused) {
sound._paused = true;
if (sound._node) {
if (this._webAudio && sound._source) {
sound._source.stop();
this._cleanupSound(sound);
} else {
sound._node.pause();
}
}
this._emit('pause', sound._id);
}
});
return this;
}
stop(id) {
this._getSounds(id).forEach(sound => {
sound._paused = true;
sound._ended = true;
if (sound._node) {
if (this._webAudio) {
if (sound._source) {
sound._source.stop();
}
} else {
sound._node.pause();
sound._node.currentTime = 0;
}
this._cleanupSound(sound);
}
this._emit('stop', sound._id);
});
return this;
}
mute(muted, id) {
if (muted === undefined) return this._muted;
if (id === undefined) this._muted = muted;
this._getSounds(id).forEach(sound => {
sound._muted = muted;
this._updateSoundVolume(sound);
this._emit('mute', sound._id);
});
return this;
}
volume(vol, id) {
if (vol === undefined) {
let sound = id ? this._sounds.find(s => s._id === id) : this._sounds[0];
return sound ? sound._volume : this._volume;
}
vol = Math.max(0, Math.min(1, vol));
if (id === undefined) this._volume = vol;
this._getSounds(id).forEach(sound => {
sound._volume = vol;
this._updateSoundVolume(sound);
this._emit('volume', sound._id);
});
return this;
}
fade(from, to, duration, id) {
this._getSounds(id).forEach(sound => {
sound._volume = from;
this._updateSoundVolume(sound);
let startTime = Date.now();
let fadeStep = () => {
let elapsed = Date.now() - startTime;
let progress = Math.min(elapsed / duration, 1);
let currentVol = from + (to - from) * progress;
sound._volume = currentVol;
this._updateSoundVolume(sound);
if (progress < 1) {
requestAnimationFrame(fadeStep);
} else {
this._emit('fade', sound._id);
}
};
requestAnimationFrame(fadeStep);
});
return this;
}
rate(rate, id) {
if (rate === undefined) {
let sound = id ? this._sounds.find(s => s._id === id) : this._sounds[0];
return sound ? sound._rate : this._rate;
}
if (id === undefined) this._rate = rate;
this._getSounds(id).forEach(sound => {
sound._rate = rate;
if (sound._node) {
if (this._webAudio && sound._source) {
sound._source.playbackRate.setValueAtTime(rate, Howler.ctx.currentTime);
} else {
sound._node.playbackRate = rate;
}
}
this._emit('rate', sound._id);
});
return this;
}
seek(seek, id) {
if (seek === undefined) {
let sound = id ? this._sounds.find(s => s._id === id) : this._sounds[0];
if (!sound) return 0;
if (this._webAudio) {
let elapsed = sound._playStart ? Howler.ctx.currentTime - sound._playStart : 0;
return sound._seek + elapsed * sound._rate;
}
return sound._node?.currentTime || 0;
}
this._getSounds(id).forEach(sound => {
let wasPlaying = !sound._paused;
if (wasPlaying) this.pause(sound._id);
sound._seek = seek;
if (!this._webAudio && sound._node) {
sound._node.currentTime = seek;
}
if (wasPlaying) this.play(sound._id);
this._emit('seek', sound._id);
});
return this;
}
playing(id) {
if (id) {
let sound = this._sounds.find(s => s._id === id);
return sound ? !sound._paused : false;
}
return this._sounds.some(s => !s._paused);
}
duration(id) {
if (id) {
let sound = this._sounds.find(s => s._id === id);
if (sound && this._sprite[sound._sprite]) {
return this._sprite[sound._sprite][1] / 1000;
}
}
return this._duration;
}
state() {
return this._state;
}
unload() {
this.stop();
this._stopFrequencyAnalysis();
Howler._howls.splice(Howler._howls.indexOf(this), 1);
this._sounds = [];
this._state = 'unloaded';
return null;
}
enableEQ(bands = 16, callback, boost = 4.0) {
if (!this._webAudio) {
console.warn('Frequency analysis requires Web Audio API');
return this;
}
this._frequencyCallback = callback;
this._frequencyBands = bands;
this._frequencyBoost = boost;
this._frequencyEnabled = true;
if (!this._analyzer) {
this._analyzer = Howler.ctx.createAnalyser();
this._analyzer.fftSize = 2048;
this._analyzer.smoothingTimeConstant = 0.3;
this._frequencyData = new Uint8Array(this._analyzer.frequencyBinCount);
this._analyzer.connect(Howler.masterGain);
}
this._rebuildEffectChain();
this._startFrequencyAnalysis();
return this;
}
disableEQ() {
this._frequencyEnabled = false;
this._stopFrequencyAnalysis();
this._rebuildEffectChain();
return this;
}
getFrequencyData() {
if (!this._analyzer || !this._frequencyEnabled) return null;
this._analyzer.getByteFrequencyData(this._frequencyData);
let bands = new Array(this._frequencyBands);
let nyquist = Howler.ctx.sampleRate / 2;
let dataLength = this._frequencyData.length;
let minFreq = 20;
let maxFreq = nyquist;
let logMin = Math.log(minFreq);
let logMax = Math.log(maxFreq);
let logRange = logMax - logMin;
for (let i = 0; i < this._frequencyBands; i++) {
// Logarithmic frequency distribution
let logStart = logMin + (i / this._frequencyBands) * logRange;
let logEnd = logMin + ((i + 1) / this._frequencyBands) * logRange;
let startFreq = Math.exp(logStart);
let endFreq = Math.exp(logEnd);
let startBin = Math.floor((startFreq / nyquist) * dataLength);
let endBin = Math.ceil((endFreq / nyquist) * dataLength);
let sum = 0;
let count = 0;
for (let j = startBin; j < Math.min(endBin, dataLength); j++) {
sum += this._frequencyData[j];
count++;
}
bands[i] = count > 0 ? (sum / count / 255) : 0; // Normalize to 0-1
}
for (let i = 0; i < bands.length; i++) {
bands[i] = Math.min(1.0, bands[i] * this._frequencyBoost);
}
return bands;
}
_startFrequencyAnalysis() {
if (!this._frequencyEnabled || this._frequencyAnimationId) return;
let analyze = () => {
if (!this._frequencyEnabled) return;
let frequencyData = this.getFrequencyData();
if (frequencyData && this._frequencyCallback) {
this._frequencyCallback(frequencyData);
}
this._frequencyAnimationId = requestAnimationFrame(analyze);
};
analyze();
}
_stopFrequencyAnalysis() {
if (this._frequencyAnimationId) {
cancelAnimationFrame(this._frequencyAnimationId);
this._frequencyAnimationId = null;
}
}
addEffect(type, options = {}) {
if (!this._webAudio) {
console.warn('Audio effects require Web Audio API');
return null;
}
let effect;
switch (type.toLowerCase()) {
case 'lowpass':
effect = Howler.ctx.createBiquadFilter();
effect.type = 'lowpass';
effect.frequency.setValueAtTime(options.frequency || 1000, Howler.ctx.currentTime);
effect.Q.setValueAtTime(options.Q || 1, Howler.ctx.currentTime);
break;
case 'highpass':
effect = Howler.ctx.createBiquadFilter();
effect.type = 'highpass';
effect.frequency.setValueAtTime(options.frequency || 300, Howler.ctx.currentTime);
effect.Q.setValueAtTime(options.Q || 1, Howler.ctx.currentTime);
break;
case 'bandpass':
effect = Howler.ctx.createBiquadFilter();
effect.type = 'bandpass';
effect.frequency.setValueAtTime(options.frequency || 1000, Howler.ctx.currentTime);
effect.Q.setValueAtTime(options.Q || 1, Howler.ctx.currentTime);
break;
case 'notch':
effect = Howler.ctx.createBiquadFilter();
effect.type = 'notch';
effect.frequency.setValueAtTime(options.frequency || 1000, Howler.ctx.currentTime);
effect.Q.setValueAtTime(options.Q || 30, Howler.ctx.currentTime);
break;
case 'delay':
effect = Howler.ctx.createDelay(options.maxDelay || 1);
effect.delayTime.setValueAtTime(options.delay || 0.3, Howler.ctx.currentTime);
break;
case 'reverb':
effect = Howler.ctx.createConvolver();
let length = Howler.ctx.sampleRate * (options.duration || 2);
let impulse = Howler.ctx.createBuffer(2, length, Howler.ctx.sampleRate);
let decay = options.decay || 2;
for (let channel = 0; channel < 2; channel++) {
let channelData = impulse.getChannelData(channel);
for (let i = 0; i < length; i++) {
channelData[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / length, decay);
}
}
effect.buffer = impulse;
break;
case 'gain':
effect = Howler.ctx.createGain();
effect.gain.setValueAtTime(options.gain || 1, Howler.ctx.currentTime);
break;
case 'distortion':
effect = Howler.ctx.createWaveShaper();
let samples = 44100;
let curve = new Float32Array(samples);
let amount = options.amount || 50;
for (let i = 0; i < samples; i++) {
let x = (i * 2) / samples - 1;
curve[i] = ((3 + amount) * x * 20 * Math.PI / 180) / (Math.PI + amount * Math.abs(x));
}
effect.curve = curve;
effect.oversample = options.oversample || '4x';
break;
default:
console.warn(`Unknown effect type: ${type}`);
return null;
}
if (effect) {
this._effects.push(effect);
this._rebuildEffectChain();
}
return effect;
}
get effects() {
return this._effects;
}
updateEffectsChain() {
this._rebuildEffectChain();
return this;
}
_rebuildEffectChain() {
this._sounds.forEach(sound => {
if (sound._node && this._webAudio) {
let startNode = sound._panner || sound._node;
this._connectSoundToChain(sound, startNode);
}
});
}
_connectSoundToChain(sound, startNode = null) {
if (!sound._node || !this._webAudio) return;
let sourceNode = startNode || sound._node;
sourceNode.disconnect();
let currentNode = sourceNode;
this._effects.forEach(effect => {
currentNode.connect(effect);
currentNode = effect;
});
if (this._environmentReverb && sound._panner) {
this._ensureEnvironmentSend(sound);
currentNode.connect(sound._environmentSend);
sound._environmentSend.connect(this._environmentReverb);
}
if (this._analyzer && this._frequencyEnabled) {
currentNode.connect(this._analyzer);
sound._analyzerConnected = true;
} else {
currentNode.connect(Howler.masterGain);
sound._analyzerConnected = false;
}
}
_ensureEnvironmentSend(sound) {
if (!sound._environmentSend) {
sound._environmentSend = Howler.ctx.createGain();
sound._environmentSend.gain.setValueAtTime(0.7, Howler.ctx.currentTime);
}
}
on(event, fn, id) {
(this._listeners[event] = this._listeners[event] || []).push({ fn, id });
return this;
}
off(event, fn, id) {
let listeners = this._listeners[event];
if (!listeners) return this;
if (!fn && !id) {
this._listeners[event] = [];
} else {
this._listeners[event] = listeners.filter(l =>
(fn && l.fn !== fn) || (id && l.id !== id)
);
}
return this;
}
once(event, fn, id) {
let onceFn = (...args) => {
fn(...args);
this.off(event, onceFn, id);
};
return this.on(event, onceFn, id);
}
_selectSource() {
for (let src of this._src) {
let ext = this._format?.[this._src.indexOf(src)] ||
src.match(/\.([^.?]+)(\?|$)/)?.[1] ||
src.match(/^data:audio\/([^;,]+)/)?.[1];
if (ext && Howler.codecs(ext)) return src;
}
return null;
}
async _loadWebAudio(url) {
try {
let arrayBuffer;
if (url.startsWith('data:')) {
let data = atob(url.split(',')[1]);
arrayBuffer = new Uint8Array(data.length);
for (let i = 0; i < data.length; i++) {
arrayBuffer[i] = data.charCodeAt(i);
}
arrayBuffer = arrayBuffer.buffer;
} else {
let response = await fetch(url, this._xhr);
arrayBuffer = await response.arrayBuffer();
}
let audioBuffer = await Howler.ctx.decodeAudioData(arrayBuffer);
this._buffer = audioBuffer;
this._duration = audioBuffer.duration;
this._finishLoad();
return this;
} catch (error) {
this._emit('loaderror', null, error.message);
throw error;
}
}
async _loadHtml5(url) {
return new Promise((resolve, reject) => {
let audio = Howler._getHtml5Audio();
let onLoad = () => {
this._duration = Math.ceil(audio.duration * 10) / 10;
this._finishLoad();
audio.removeEventListener('canplaythrough', onLoad);
audio.removeEventListener('error', onError);
resolve(this);
};
let onError = () => {
let error = `Failed to load: ${url}`;
audio.removeEventListener('canplaythrough', onLoad);
audio.removeEventListener('error', onError);
this._emit('loaderror', null, error);
reject(new Error(error));
};
audio.addEventListener('canplaythrough', onLoad);
audio.addEventListener('error', onError);
audio.src = url;
audio.load();
});
}
_finishLoad() {
if (!Object.keys(this._sprite).length) {
this._sprite = { __default: [0, this._duration * 1000] };
}
this._state = 'loaded';
this._emit('load');
this._processQueue();
}
_processQueue() {
if (this._queue.length) {
let { action } = this._queue.shift();
action();
this._processQueue();
}
}
_getSound() {
let sound = this._sounds.find(s => s._ended);
if (!sound) {
if (this._sounds.length >= this._pool) {
return this._sounds.find(s => s._paused) || this._sounds[0];
}
sound = this._createSound();
}
return this._resetSound(sound);
}
_createSound() {
let sound = {
_id: ++Howler._counter,
_volume: this._volume,
_rate: this._rate,
_seek: 0,
_paused: true,
_ended: true,
_muted: this._muted,
_sprite: '__default'
};
this._sounds.push(sound);
this._createSoundNode(sound);
return sound;
}
_resetSound(sound) {
Object.assign(sound, {
_volume: this._volume,
_rate: this._rate,
_seek: 0,
_paused: true,
_ended: true,
_muted: this._muted,
_sprite: '__default'
});
return sound;
}
_createSoundNode(sound) {
if (this._webAudio) {
sound._node = Howler.ctx.createGain();
if (this.spatial?.enabled) {
sound._panner = Howler.ctx.createPanner();
// Set the panning model (this doesn't change dynamically)
sound._panner.panningModel = this.spatial.panningModel;
sound._node.connect(sound._panner);
this._updateSoundSpatial(sound);
if (Howler._environment) {
this._updateEnvironment();
}
this._connectSoundToChain(sound, sound._panner);
} else {
this._connectSoundToChain(sound);
}
} else {
sound._node = Howler._getHtml5Audio();
sound._node.src = this._src;
}
}
_playSound(sound) {
if (!sound || this._state !== 'loaded') return null;
let sprite = this._sprite[sound._sprite];
if (!sprite) return null;
let [start, duration] = sprite;
let seek = Math.max(0, sound._seek || start / 1000);
sound._paused = false;
sound._ended = false;
if (this._webAudio) {
this._playWebAudio(sound, seek, duration / 1000);
} else {
this._playHtml5(sound, seek);
}
this._emit('play', sound._id);
return sound._id;
}
_playWebAudio(sound, seek, duration) {
this._cleanupSound(sound);
sound._source = Howler.ctx.createBufferSource();
sound._source.buffer = this._buffer;
sound._source.connect(sound._node);
sound._source.playbackRate.setValueAtTime(sound._rate, Howler.ctx.currentTime);
sound._playStart = Howler.ctx.currentTime;
if (sound._loop) {
sound._source.loop = true;
sound._source.start(0, seek);
} else {
// Fix: duration is the sprite duration, not total audio duration
sound._source.start(0, seek, duration);
sound._source.onended = () => this._onSoundEnd(sound);
}
this._updateSoundVolume(sound);
}
_playHtml5(sound, seek) {
let { _node: node } = sound;
node.currentTime = seek;
node.volume = this._getEffectiveVolume(sound);
node.playbackRate = sound._rate;
node.muted = sound._muted || this._muted || Howler._muted;
let promise = node.play();
if (promise) {
promise.catch(() => this._emit('playerror', sound._id, 'Playback failed'));
}
if (!sound._loop) {
node.onended = () => this._onSoundEnd(sound);
}
}
_onSoundEnd(sound) {
sound._paused = true;
sound._ended = true;
this._emit('end', sound._id);
if (sound._loop) {
sound._ended = false;
this._playSound(sound);
}
}
_updateSoundVolume(sound) {
if (!sound._node) return;
let volume = this._getEffectiveVolume(sound);
if (this._webAudio) {
sound._node.gain.setValueAtTime(volume, Howler.ctx.currentTime);
} else {
sound._node.volume = volume;
}
}
_getEffectiveVolume(sound) {
if (sound._muted || this._muted || Howler._muted) return 0;
return sound._volume * this._volume * Howler._volume;
}
_updateVolume() {
this._sounds.forEach(sound => this._updateSoundVolume(sound));
}
_cleanupSound(sound) {
if (sound._source) {
sound._source.disconnect();
sound._source = null;
}
if (sound._occlusionFilter) {
sound._occlusionFilter.disconnect();
sound._occlusionFilter = null;
}
}
_getSounds(id) {
return id ? this._sounds.filter(s => s._id === id) : this._sounds;
}
_emit(event, id, data) {
let listeners = this._listeners[event];
if (!listeners) return;
listeners.forEach(({ fn, id: listenerId }) => {
if (!listenerId || listenerId === id) {
setTimeout(() => fn(id, data), 0);
}
});
}
_updateSpatial() {
if (!this.spatial?.enabled) return;
this._sounds.forEach(sound => {
if (sound._panner) {
this._updateSoundSpatial(sound);
}
});
}
_updateSoundSpatial(sound) {
if (!sound._panner || !this.spatial?.enabled) return;
let { position, orientation, velocity, distance, cone, occlusion, obstruction } = this.spatial;
let p = sound._panner;
if (p.positionX) {
p.positionX.setValueAtTime(position.x, Howler.ctx.currentTime);
p.positionY.setValueAtTime(position.y, Howler.ctx.currentTime);
p.positionZ.setValueAtTime(position.z, Howler.ctx.currentTime);
p.orientationX.setValueAtTime(orientation.x, Howler.ctx.currentTime);
p.orientationY.setValueAtTime(orientation.y, Howler.ctx.currentTime);
p.orientationZ.setValueAtTime(orientation.z, Howler.ctx.currentTime);
} else {
p.setPosition(position.x, position.y, position.z);
p.setOrientation(orientation.x, orientation.y, orientation.z);
}
if (p.setVelocity && velocity) {
p.setVelocity(velocity.x, velocity.y, velocity.z);
}
p.refDistance = distance.ref;
p.maxDistance = distance.max;
p.rolloffFactor = distance.rolloff;
p.distanceModel = distance.model;
p.coneInnerAngle = cone.inner;
p.coneOuterAngle = cone.outer;
p.coneOuterGain = cone.outerGain;
this._applyOcclusion(sound, occlusion || 0, obstruction || 0);
}
_applyOcclusion(sound, occlusion, obstruction) {
if (!sound._occlusionFilter && (occlusion > 0 || obstruction > 0)) {
sound._occlusionFilter = Howler.ctx.createBiquadFilter();
sound._occlusionFilter.type = 'lowpass';
sound._node.disconnect();
sound._node.connect(sound._occlusionFilter);
sound._occlusionFilter.connect(sound._panner || Howler.masterGain);
}
if (sound._occlusionFilter) {
let freq = 20000 * (1 - Math.max(occlusion, obstruction) * 0.9);
let gain = 1 - occlusion * 0.8;
sound._occlusionFilter.frequency.setValueAtTime(freq, Howler.ctx.currentTime);
sound._node.gain.setValueAtTime(gain * this._getEffectiveVolume(sound), Howler.ctx.currentTime);
}
}
_updateEnvironment() {
if (!Howler._environment || !this._webAudio) return;
if (!this._environmentReverb) {
this._environmentReverb = Howler.ctx.createConvolver();
let { size, decay, dampening } = Howler._environment;
let length = Howler.ctx.sampleRate * (size === 'large' ? 6 : size === 'medium' ? 3 : 1.5);
let impulse = Howler.ctx.createBuffer(2, length, Howler.ctx.sampleRate);
for (let channel = 0; channel < 2; channel++) {
let channelData = impulse.getChannelData(channel);
for (let i = 0; i < length; i++) {
let progress = i / length;
let sample = 0;
if (i < Howler.ctx.sampleRate * 0.2) {
for (let j = 0; j < 8; j++) {
let delay = (j + 1) * 0.02 * Howler.ctx.sampleRate;
if (Math.abs(i - delay) < 100) {
sample += (Math.random() * 2 - 1) * 0.3 * Math.exp(-j * 0.5);
}
}
}
let envelope = Math.pow(1 - progress, decay);
let reverb = (Math.random() * 2 - 1) * envelope;
sample += reverb * (1 - dampening + Math.random() * dampening * 0.3);
if (size === 'large') {
let lowFreq = Math.sin(i * 0.01) * envelope * 0.2;
sample += lowFreq;
}
channelData[i] = Math.tanh(sample * 0.8);
}
}
this._environmentReverb.buffer = impulse;
this._environmentGain = Howler.ctx.createGain();
this._environmentGain.gain.setValueAtTime(0.8, Howler.ctx.currentTime);
this._environmentReverb.connect(this._environmentGain);
this._environmentGain.connect(Howler.masterGain);
}
this._rebuildEffectChain();
}
}
let Howler = new HowlerGlobal();
return { Howler, Howl };
}));