using UnityEngine; namespace AlicizaX.Audio.Runtime { internal sealed class AudioAgent { private const float DefaultFadeOutSeconds = 0.15f; private const float MinFadeOutSeconds = 0.0001f; private const float MaxCutoffFrequency = 22000f; private AudioService _service; private AudioCategory _category; private AudioSourceObject _sourceObject; private AudioSource _source; private AudioLowPassFilter _lowPassFilter; private AudioClipCacheEntry _clipEntry; private Transform _transform; private Transform _followTarget; private Vector3 _followOffset; private AudioAgentRuntimeState _state; private bool _spatial; private bool _occluded; private bool _loop; private int _generation; private ulong _handle; private float _baseVolume; private float _pitch; private float _fadeTimer; private float _fadeDuration; private float _startedAt; private float _nextOcclusionCheckTime; internal int Index { get; private set; } internal int GlobalIndex { get; private set; } internal int HeapIndex { get; set; } internal int ActiveIndex { get; set; } internal ulong Handle => _handle; internal int Generation => _generation; internal bool IsFree => _state == AudioAgentRuntimeState.Free; internal bool IsPlayingState => _state == AudioAgentRuntimeState.Playing || _state == AudioAgentRuntimeState.Loading || _state == AudioAgentRuntimeState.Paused || _state == AudioAgentRuntimeState.FadingOut; internal float StartedAt => _startedAt; internal void Initialize(AudioService service, AudioCategory category, int index, int globalIndex, AudioSourceObject sourceObject) { _service = service; _category = category; Index = index; GlobalIndex = globalIndex; HeapIndex = -1; ActiveIndex = -1; BindSource(sourceObject); ResetState(); } internal ulong Play(AudioPlayRequest request) { StopImmediate(false); _generation++; if (_generation == int.MaxValue) { _generation = 1; } _handle = _service.AllocateHandle(this); _state = AudioAgentRuntimeState.Loading; _startedAt = Time.realtimeSinceStartup; _baseVolume = Mathf.Clamp01(request.Volume); _pitch = request.Pitch <= 0f ? 1f : request.Pitch; _fadeDuration = request.FadeOutSeconds > 0f ? request.FadeOutSeconds : DefaultFadeOutSeconds; _loop = request.Loop; _spatial = request.Spatial || request.FollowTarget != null || request.UseWorldPosition; _followTarget = request.FollowTarget; _followOffset = request.Position; _nextOcclusionCheckTime = 0f; _occluded = false; ApplySourceSettings(request); _category.MarkOccupied(this); if (request.Clip != null) { StartClip(request.Clip); return _handle; } if (string.IsNullOrEmpty(request.Address)) { StopImmediate(true); return 0UL; } AudioClipCacheEntry entry = _service.RequestClip(request.Address, request.Async, request.CacheClip, this, _generation); if (entry != null) { OnClipReady(entry, _generation); } return _handle; } internal bool OnClipReady(AudioClipCacheEntry entry, int generation) { if (_state != AudioAgentRuntimeState.Loading || generation != _generation || entry == null || entry.Clip == null) { return false; } _clipEntry = entry; _service.RetainClip(entry); StartClip(entry.Clip); return true; } internal void OnClipLoadFailed(int generation) { if (_state == AudioAgentRuntimeState.Loading && generation == _generation) { StopImmediate(true); } } internal void Stop(bool fadeout) { if (_state == AudioAgentRuntimeState.Free) { return; } if (!fadeout || _state == AudioAgentRuntimeState.Loading) { StopImmediate(true); return; } _fadeTimer = _fadeDuration; _state = AudioAgentRuntimeState.FadingOut; } internal void Pause() { if (_state != AudioAgentRuntimeState.Playing || _source == null) { return; } _source.Pause(); _state = AudioAgentRuntimeState.Paused; } internal void Resume() { if (_state != AudioAgentRuntimeState.Paused || _source == null) { return; } _source.UnPause(); _state = AudioAgentRuntimeState.Playing; } internal void Update(float deltaTime) { if (_state == AudioAgentRuntimeState.Free) { return; } UpdateFollowTarget(); UpdateOcclusion(); if (_state == AudioAgentRuntimeState.Playing) { if (!_loop && _source != null && !_source.isPlaying) { StopImmediate(true); } return; } if (_state != AudioAgentRuntimeState.FadingOut) { return; } _fadeTimer -= deltaTime; if (_fadeTimer <= 0f) { StopImmediate(true); return; } float fadeScale = _fadeTimer / Mathf.Max(_fadeDuration, MinFadeOutSeconds); ApplyRuntimeVolume(fadeScale); } internal void Shutdown() { StopImmediate(true); _sourceObject = null; _source = null; _lowPassFilter = null; _transform = null; _service = null; _category = null; } internal void FillDebugInfo(AudioAgentDebugInfo info) { if (info == null) { return; } info.Type = _category != null ? _category.Type : AudioType.Sound; info.State = _state; info.Index = Index; info.GlobalIndex = GlobalIndex; info.ActiveIndex = ActiveIndex; info.Handle = _handle; info.Address = _clipEntry != null ? _clipEntry.Address : null; info.Clip = _source != null ? _source.clip : null; info.FollowTarget = _followTarget; info.Position = _transform != null ? _transform.position : Vector3.zero; info.Loop = _loop; info.Spatial = _spatial; info.Occluded = _occluded; info.Volume = _source != null ? _source.volume : 0f; info.Pitch = _source != null ? _source.pitch : 0f; info.SpatialBlend = _source != null ? _source.spatialBlend : 0f; info.MinDistance = _source != null ? _source.minDistance : 0f; info.MaxDistance = _source != null ? _source.maxDistance : 0f; info.StartedAt = _startedAt; } private void BindSource(AudioSourceObject sourceObject) { _sourceObject = sourceObject; _source = sourceObject.Source; _lowPassFilter = sourceObject.LowPassFilter; _transform = _source.transform; } private void StartClip(AudioClip clip) { if (_source == null || clip == null) { StopImmediate(true); return; } _source.clip = clip; _source.loop = _loop; ApplyRuntimeVolume(1f); _source.Play(); _state = AudioAgentRuntimeState.Playing; } private void StopImmediate(bool notifyCategory) { if (_state == AudioAgentRuntimeState.Free) { return; } _generation++; if (_generation == int.MaxValue) { _generation = 1; } if (_source != null) { _source.Stop(); _source.clip = null; } ReleaseClip(); ulong handle = _handle; if (handle != 0) { _service.ReleaseHandle(handle, this); } ResetState(); if (notifyCategory) { _category.MarkFree(this); } } private void ReleaseClip() { AudioClipCacheEntry entry = _clipEntry; if (entry != null) { _clipEntry = null; _service.ReleaseClip(entry); } } private void ResetState() { _state = AudioAgentRuntimeState.Free; _followTarget = null; _followOffset = Vector3.zero; _spatial = false; _occluded = false; _loop = false; _handle = 0; _baseVolume = 1f; _pitch = 1f; _fadeTimer = 0f; _fadeDuration = 0f; _startedAt = 0f; _nextOcclusionCheckTime = 0f; if (_source != null) { _source.volume = 1f; _source.pitch = 1f; _source.loop = false; } if (_lowPassFilter != null) { _lowPassFilter.enabled = false; _lowPassFilter.cutoffFrequency = MaxCutoffFrequency; } } private void ApplySourceSettings(AudioPlayRequest request) { AudioGroupConfig config = _category.Config; _source.playOnAwake = false; _source.mute = false; _source.bypassEffects = false; _source.bypassListenerEffects = false; _source.bypassReverbZones = false; _source.priority = config.SourcePriority; _source.pitch = _pitch; _source.rolloffMode = request.OverrideSpatialSettings ? request.RolloffMode : config.RolloffMode; _source.minDistance = request.OverrideSpatialSettings ? request.MinDistance : config.MinDistance; _source.maxDistance = request.OverrideSpatialSettings ? request.MaxDistance : config.MaxDistance; _source.dopplerLevel = config.DopplerLevel; _source.spread = config.Spread; _source.reverbZoneMix = config.ReverbZoneMix; _source.outputAudioMixerGroup = _category.MixerGroup; _source.spatialBlend = _spatial ? ResolveSpatialBlend(request, config) : 0f; Transform transform = _source.transform; if (_followTarget != null) { transform.SetParent(_category.InstanceRoot, false); transform.position = _followTarget.position + _followOffset; transform.rotation = _followTarget.rotation; } else { transform.SetParent(_category.InstanceRoot, false); if (request.UseWorldPosition) { transform.position = request.Position; } else { transform.localPosition = Vector3.zero; } } } private static float ResolveSpatialBlend(AudioPlayRequest request, AudioGroupConfig config) { if (request.SpatialBlend >= 0f) { return Mathf.Clamp01(request.SpatialBlend); } return config.SpatialBlend; } private void UpdateFollowTarget() { if (_followTarget == null) { return; } if (_transform == null) { return; } if (!_followTarget.gameObject.activeInHierarchy) { StopImmediate(true); return; } _transform.position = _followTarget.position + _followOffset; _transform.rotation = _followTarget.rotation; } private void UpdateOcclusion() { AudioGroupConfig config = _category.Config; if (!_spatial || !config.OcclusionEnabled || _transform == null) { return; } Transform listener = _service.ListenerTransform; if (listener == null) { return; } float now = Time.realtimeSinceStartup; if (now < _nextOcclusionCheckTime) { return; } _nextOcclusionCheckTime = now + config.OcclusionCheckInterval; Vector3 origin = _transform.position; Vector3 target = listener.position; Vector3 direction = target - origin; float distance = direction.magnitude; if (distance <= 0.01f) { SetOccluded(false, config); return; } bool occluded = Physics.Raycast(origin, direction / distance, distance, config.OcclusionMask, QueryTriggerInteraction.Ignore); SetOccluded(occluded, config); } private void SetOccluded(bool occluded, AudioGroupConfig config) { if (_occluded == occluded) { return; } _occluded = occluded; if (_lowPassFilter != null) { _lowPassFilter.enabled = occluded; _lowPassFilter.cutoffFrequency = occluded ? config.OcclusionLowPassCutoff : MaxCutoffFrequency; } ApplyRuntimeVolume(_state == AudioAgentRuntimeState.FadingOut ? _fadeTimer / Mathf.Max(_fadeDuration, MinFadeOutSeconds) : 1f); } private void ApplyRuntimeVolume(float fadeScale) { if (_source == null) { return; } float occlusionScale = _occluded ? _category.Config.OcclusionVolumeMultiplier : 1f; _source.volume = _baseVolume * occlusionScale * fadeScale; } } }