(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 };

}));
