using System;
using System.Collections.Generic;
using DanroJump.Settings;
using UnityEngine;
using UnityEngine.Audio;
using Zenject;
using Object = UnityEngine.Object;
namespace DanroJump.Audio
{
///
/// Централизованный проигрыватель звуков: применяет service-volume, routes events в mixer groups и держит источники звука.
///
public sealed class GameAudioService : IGameAudioService, IInitializable, IDisposable
{
public const string MusicVolumeParameter = "MusicVolume";
public const string SoundsVolumeParameter = "SoundsVolume";
public const float MutedDecibels = -80f;
private const int SpatialSourcePoolSize = 8;
private readonly GameAudioSettings settings;
private readonly IReadOnlyServiceSettings serviceSettings;
private readonly List spatialSources = new();
private GameObject root;
private AudioSource musicSource;
private AudioSource uiSource;
private AudioSource gameplaySource;
private int nextSpatialSourceIndex;
private GameMusicCue? currentMusicCue;
private bool subscribedToSettings;
public GameAudioService(
[InjectOptional] GameAudioSettings settings = null,
[InjectOptional] IReadOnlyServiceSettings serviceSettings = null)
{
this.settings = settings;
this.serviceSettings = serviceSettings;
}
public bool IsReady => HasPlayableConfiguration();
public bool IsMusicPlaying => musicSource != null && musicSource.isPlaying;
public GameMusicCue? CurrentMusicCue => currentMusicCue;
public void Initialize()
{
if (!IsReady)
{
Debug.LogWarning($"{nameof(GameAudioService)} has no complete playable audio configuration.");
}
EnsureSources();
SubscribeToSettings();
ApplyServiceVolumes();
if (settings == null || settings.PlayMusicOnInitialize)
{
StartMusic();
}
}
public void Dispose()
{
UnsubscribeFromSettings();
if (root != null)
{
if (Application.isPlaying)
{
Object.Destroy(root);
}
else
{
Object.DestroyImmediate(root);
}
root = null;
}
}
public void Play(GameAudioEvent eventId)
{
PlayInternal(eventId, null);
}
public void Play(GameAudioEvent eventId, Vector3 worldPosition)
{
PlayInternal(eventId, worldPosition);
}
public void StartMusic()
{
if (settings == null)
{
return;
}
if (settings.TryGetDefaultMusicTrack(out var track))
{
currentMusicCue = track.Cue;
PlayMusicTrack(track);
return;
}
StartLegacyMusicLoop();
}
public void PlayMusic(GameMusicCue cue)
{
if (settings == null || !settings.TryGetMusicTrack(cue, out var track))
{
return;
}
currentMusicCue = cue;
PlayMusicTrack(track);
}
public void StopMusic()
{
if (musicSource != null)
{
musicSource.Stop();
musicSource.clip = null;
currentMusicCue = null;
}
}
public void ApplyServiceVolumes()
{
if (settings?.Mixer == null)
{
return;
}
var musicVolume = serviceSettings != null ? serviceSettings.GetFloat(ServiceSettingsKeys.MusicVolume) : 1f;
var soundsVolume = serviceSettings != null ? serviceSettings.GetFloat(ServiceSettingsKeys.SoundsVolume) : 1f;
settings.Mixer.SetFloat(MusicVolumeParameter, LinearToDecibels(musicVolume));
settings.Mixer.SetFloat(SoundsVolumeParameter, LinearToDecibels(soundsVolume));
}
public static float LinearToDecibels(float value)
{
var clamped = Mathf.Clamp01(value);
return clamped <= 0.0001f ? MutedDecibels : Mathf.Log10(clamped) * 20f;
}
private void StartLegacyMusicLoop()
{
if (settings == null || settings.MusicLoop == null)
{
return;
}
EnsureSources();
musicSource.clip = settings.MusicLoop;
musicSource.loop = true;
musicSource.volume = settings.MusicLoopVolume;
musicSource.pitch = 1f;
musicSource.outputAudioMixerGroup = settings.MusicGroup;
currentMusicCue = null;
if (!musicSource.isPlaying)
{
musicSource.Play();
}
}
private void PlayMusicTrack(GameMusicTrack track)
{
if (track == null || track.Clip == null)
{
return;
}
EnsureSources();
if (musicSource.isPlaying && musicSource.clip == track.Clip)
{
musicSource.volume = track.Volume;
musicSource.loop = track.Loop;
musicSource.outputAudioMixerGroup = track.OutputGroupOverride != null
? track.OutputGroupOverride
: settings.MusicGroup;
return;
}
musicSource.Stop();
musicSource.clip = track.Clip;
musicSource.loop = track.Loop;
musicSource.volume = track.Volume;
musicSource.pitch = 1f;
musicSource.outputAudioMixerGroup = track.OutputGroupOverride != null
? track.OutputGroupOverride
: settings.MusicGroup;
musicSource.Play();
}
private void PlayInternal(GameAudioEvent eventId, Vector3? worldPosition)
{
if (settings == null || !settings.TryGetBinding(eventId, out var binding))
{
return;
}
var clip = binding.GetRandomClip();
if (clip == null)
{
return;
}
EnsureSources();
var source = ResolveSource(binding, worldPosition);
source.outputAudioMixerGroup = binding.OutputGroupOverride != null
? binding.OutputGroupOverride
: settings.ResolveGroup(binding.Channel);
source.pitch = binding.RandomPitch;
source.spatialBlend = binding.Spatial || worldPosition.HasValue ? 1f : 0f;
if (worldPosition.HasValue)
{
source.transform.position = worldPosition.Value;
}
source.PlayOneShot(clip, binding.Volume);
}
private AudioSource ResolveSource(GameAudioClipBinding binding, Vector3? worldPosition)
{
if (binding.Spatial || worldPosition.HasValue)
{
return ResolveSpatialSource();
}
return binding.Channel switch
{
GameAudioChannel.Music => musicSource,
GameAudioChannel.Ui => uiSource,
_ => gameplaySource
};
}
private void EnsureSources()
{
if (root != null)
{
return;
}
root = new GameObject("Game Audio Service");
if (Application.isPlaying)
{
Object.DontDestroyOnLoad(root);
}
musicSource = CreateSource("Music", GameAudioChannel.Music);
uiSource = CreateSource("UI", GameAudioChannel.Ui);
gameplaySource = CreateSource("Gameplay", GameAudioChannel.Gameplay);
for (var index = 0; index < SpatialSourcePoolSize; index++)
{
var spatialSource = CreateSource($"Spatial {index + 1}", GameAudioChannel.Gameplay);
spatialSource.spatialBlend = 1f;
spatialSources.Add(spatialSource);
}
}
private AudioSource ResolveSpatialSource()
{
if (spatialSources.Count == 0)
{
return gameplaySource;
}
var source = spatialSources[nextSpatialSourceIndex % spatialSources.Count];
nextSpatialSourceIndex = (nextSpatialSourceIndex + 1) % spatialSources.Count;
return source;
}
private AudioSource CreateSource(string sourceName, GameAudioChannel channel)
{
var sourceObject = new GameObject(sourceName);
sourceObject.transform.SetParent(root.transform, false);
var source = sourceObject.AddComponent();
source.playOnAwake = false;
source.loop = false;
source.outputAudioMixerGroup = settings != null ? settings.ResolveGroup(channel) : null;
return source;
}
private bool HasPlayableConfiguration()
{
if (settings == null ||
settings.Mixer == null ||
settings.MusicGroup == null ||
settings.ResolveGroup(GameAudioChannel.Sfx) == null ||
settings.ResolveGroup(GameAudioChannel.Ui) == null ||
settings.ResolveGroup(GameAudioChannel.Gameplay) == null ||
settings.ResolveGroup(GameAudioChannel.Ambience) == null)
{
return false;
}
var bindings = settings.EventBindings;
if (bindings == null)
{
return false;
}
for (var index = 0; index < bindings.Count; index++)
{
if (bindings[index] != null && bindings[index].HasClip)
{
return true;
}
}
return false;
}
private void SubscribeToSettings()
{
if (subscribedToSettings || serviceSettings == null)
{
return;
}
serviceSettings.SettingsLoaded += HandleSettingsLoaded;
serviceSettings.SettingChanged += HandleSettingChanged;
subscribedToSettings = true;
}
private void UnsubscribeFromSettings()
{
if (!subscribedToSettings || serviceSettings == null)
{
subscribedToSettings = false;
return;
}
serviceSettings.SettingsLoaded -= HandleSettingsLoaded;
serviceSettings.SettingChanged -= HandleSettingChanged;
subscribedToSettings = false;
}
private void HandleSettingsLoaded()
{
ApplyServiceVolumes();
}
private void HandleSettingChanged(string key, ServiceSettingValue value)
{
if (string.Equals(key, ServiceSettingsKeys.MusicVolume, StringComparison.Ordinal) ||
string.Equals(key, ServiceSettingsKeys.SoundsVolume, StringComparison.Ordinal))
{
ApplyServiceVolumes();
}
}
}
}