📗

【Unity】Unityサウンドを使用したAudioManager

2024/08/29に公開

はじめに

初めての方も、そうでない方もこんにちは!
現役ゲームプログラマーのたむぼーです。
自己紹介を載せているので、気になる方は見ていただければ嬉しいです!

今回は
 Unityサウンドを使用したAudioManager
したので紹介します

https://zenn.dev/tmb/articles/1072f8ea010299

AudioManagerについて

使い方

// BGM
// _fadeDuration = フェード時間(float)
await AudioManager.Instance.Play(BgmKeys.Home, _fadeDuration);

// SE
string tag = AudioManager.Instance?.Play(SeKeys.CommonButton);
// 以下のスクリプトをオブジェクトにアタッチする事で
// その対象が出たときにオーディオを再生できます
・PlayBgm
・PlaySe
・PlayJingle
・PlayVoice

スクリプト

オーディオマネージャー(自動生成)
AudioManager.cs
using System.Collections.Generic;
using UnityEngine;
using Cysharp.Threading.Tasks;
using System.Threading;
using System;
using System.Linq;
using Utils; // 詳細は後述してます

namespace AudioSystem
{
    sealed public class AudioManager : MonoBehaviour
    {
        private static bool _isOnDestroy = false;
        private static GameObject _audioManagerObject;
        private static AudioManager _instance;
        private static AudioListener _audioListener;
        public static AudioManager Instance
        {
            get
            {
                if (_isOnDestroy)
                {
                    return null;
                }
                AudioListener audioListener = Camera.main?.GetComponent<AudioListener>();
                if (_audioListener != null && audioListener !=  null)
                {
                    Destroy(_audioListener);
                    _audioListener = null;
                }
                if (_audioListener == null && audioListener == null && _audioManagerObject != null)
                {
                    _audioListener = _audioManagerObject.GetOrAddComponent<AudioListener>();
                }
                if (_instance == null)
                {
                    _instance = FindObjectOfType<AudioManager>();
                    if (_instance == null)
                    {
                        _audioManagerObject = new GameObject("AudioManager");
                        _instance = _audioManagerObject.GetOrAddComponent<AudioManager>();
                        if (audioListener == null)
                        {
                            _audioListener = _audioManagerObject.GetOrAddComponent<AudioListener>();
                        }
                        DontDestroyOnLoad(_audioManagerObject);
                    }
                }
                return _instance;
            }
        }

        private AudioSettings _audioSettings = null;
        private AudioSource[] _bgmAudioSources = null;
        private Queue<AudioSource> _seAudioSourcePool = null;
        private Queue<AudioSource> _jingleAudioSourcePool = null;
        private Queue<AudioSource> _voiceAudioSourcePool = null;
        private List<(string, SeKeys, AudioSource, CancellationTokenHelper)> _seActiveAudioSources = null;
        private List<(string, JingleKeys, AudioSource, CancellationTokenHelper)> _jingleActiveAudioSources = null;
        private List<(string, VoiceKeys, AudioSource, CancellationTokenHelper)> _voiceActiveAudioSources = null;
        private BgmKeys _currentBgmKey = BgmKeys.None;
        private float _currentFadeDuration = 0.0f;
        private CancellationTokenHelper _bgmCtsHelper = null;

        private bool _isInitialize = false;
        private int _currentBgmIndex = 0;

        /// <summary>
        /// 破壊時
        /// </summary>
        private void OnDestroy()
        {
            _isOnDestroy = true;

            _instance = null;
            _audioListener = null;

            if (_bgmCtsHelper != null)
            {
                _bgmCtsHelper.Dispose();
            }

            if (_seActiveAudioSources != null)
            {
                foreach ((string tag, SeKeys key, AudioSource audioSource, CancellationTokenHelper ctsHelper) seActiveAudioSource in _seActiveAudioSources)
                {
                    if (seActiveAudioSource.ctsHelper != null)
                    {
                        seActiveAudioSource.ctsHelper.Dispose();
                    }
                }
            }

            if (_jingleActiveAudioSources != null)
            {
                foreach ((string tag, JingleKeys key, AudioSource audioSource, CancellationTokenHelper ctsHelper) jingleActiveAudioSources in _jingleActiveAudioSources)
                {
                    if (jingleActiveAudioSources.ctsHelper != null)
                    {
                        jingleActiveAudioSources.ctsHelper.Dispose();
                    }
                }
            }

            if (_voiceActiveAudioSources != null)
            {
                foreach ((string tag, VoiceKeys key, AudioSource audioSource, CancellationTokenHelper ctsHelper) voiceActiveAudioSources in _voiceActiveAudioSources)
                {
                    if (voiceActiveAudioSources.ctsHelper != null)
                    {
                        voiceActiveAudioSources.ctsHelper.Dispose();
                    }
                }
            }
        }

        /// <summary>
        /// Setup
        /// </summary>
        public void Setup()
        {
            if (_isInitialize)
            {
                return;
            }

            _bgmAudioSources = new AudioSource[AudioSettings.Instance.BgmAudioSourcePoolSize];
            for (int i = 0; i < AudioSettings.Instance.BgmAudioSourcePoolSize; i++)
            {
                AudioSource audioSource = gameObject.AddComponent<AudioSource>();
                audioSource.loop = true;
                audioSource.volume = AudioSettings.Instance.BgmVolume.max;
                audioSource.playOnAwake = false;
                _bgmAudioSources[i] = audioSource;
            }

            _seAudioSourcePool = new Queue<AudioSource>();
            _seActiveAudioSources = new List<(string, SeKeys, AudioSource, CancellationTokenHelper)>();
            for (int i = 0; i < AudioSettings.Instance.SeAudioSourcePoolSize; i++)
            {
                AudioSource audioSource = gameObject.AddComponent<AudioSource>();
                audioSource.loop = false;
                audioSource.volume = AudioSettings.Instance.SeVolume.max;
                audioSource.playOnAwake = false;
                _seAudioSourcePool.Enqueue(audioSource);
            }

            _jingleAudioSourcePool = new Queue<AudioSource>();
            _jingleActiveAudioSources = new List<(string, JingleKeys, AudioSource, CancellationTokenHelper)>();
            for (int i = 0; i < AudioSettings.Instance.JingleAudioSourcePoolSize; i++)
            {
                AudioSource audioSource = gameObject.AddComponent<AudioSource>();
                audioSource.loop = false;
                audioSource.volume = AudioSettings.Instance.JingleVolume.max;
                audioSource.playOnAwake = false;
                _jingleAudioSourcePool.Enqueue(audioSource);
            }

            _voiceAudioSourcePool = new Queue<AudioSource>();
            _voiceActiveAudioSources = new List<(string, VoiceKeys, AudioSource, CancellationTokenHelper)>();
            for (int i = 0; i < AudioSettings.Instance.VoiceAudioSourcePoolSize; i++)
            {
                AudioSource audioSource = gameObject.AddComponent<AudioSource>();
                audioSource.loop = false;
                audioSource.volume = AudioSettings.Instance.VoiceVolume.max;
                audioSource.playOnAwake = false;
                _voiceAudioSourcePool.Enqueue(audioSource);
            }

            _isInitialize = true;
        }

        /// <summary>
        /// 現在再生中のBGMを取得
        /// </summary>
        public BgmKeys GetCurrentBgm()
        {
            return _currentBgmKey;
        }

        /// <summary>
        /// 現在再生中のBGMを取得
        /// </summary>
        public float GetCurrentFadeDuration()
        {
            return _currentFadeDuration;
        }

        /// <summary>
        /// Play
        /// </summary>
        public async UniTask Play(BgmKeys key, float fadeDuration = 0.0f)
        {
            if (AudioSettings.Instance == null)
            {
                return;
            }

            Setup();

            if (_bgmCtsHelper != null)
            {
                _bgmCtsHelper.Dispose();
            }
            _bgmCtsHelper = new CancellationTokenHelper();

            _currentBgmKey = key;
            _currentFadeDuration = fadeDuration;

            if (AudioSettings.Instance.BgmClipList.TryGetValue(key, out AudioClip clip))
            {
                int nextIndex = (_currentBgmIndex + 1) % 2;
                AudioSource currentSource = _bgmAudioSources[_currentBgmIndex];
                AudioSource nextSource = _bgmAudioSources[nextIndex];

                if (fadeDuration > 0)
                {
                    nextSource.clip = clip;
                    nextSource.volume = 0;
                    nextSource.Play();
                    if (currentSource.isPlaying)
                    {
                        currentSource.Stop();
                    }

                    await FadeIn(nextSource, fadeDuration, _bgmCtsHelper.Token);
                }
                else
                {
                    nextSource.clip = clip;
                    nextSource.volume = AudioSettings.Instance.BgmVolume.max;
                    nextSource.Play();
                    if (currentSource.isPlaying)
                    {
                        currentSource.Stop();
                    }
                }

                _currentBgmIndex = nextIndex;
            }
        }

        /// <summary>
        /// FadeOut
        /// </summary>
        private async UniTask FadeOut(AudioSource source, float duration, CancellationToken token)
        {
            float startVolume = source.volume;

            try
            {
                while (source.volume > 0)
                {
                    source.volume -= AudioSettings.Instance.BgmVolume.fadeRate * Time.deltaTime / duration;
                    await UniTask.Yield(token);
                }
            }
            catch (OperationCanceledException)
            {
                if (source != null)
                {
                    source.volume = 0;
                    source.Stop();
                }
                return;
            }

            source.volume = 0;
            source.Stop();
        }

        /// <summary>
        /// FadeIn
        /// </summary>
        private async UniTask FadeIn(AudioSource source, float duration, CancellationToken token)
        {
            float startVolume = AudioSettings.Instance.BgmVolume.min;
            source.volume = startVolume;

            try
            {
                while (source.volume < AudioSettings.Instance.BgmVolume.max)
                {
                    source.volume += AudioSettings.Instance.BgmVolume.fadeRate * Time.deltaTime / duration;
                    await UniTask.Yield(token);
                }
            }
            catch (OperationCanceledException)
            {
                if (source != null)
                {
                    source.volume = AudioSettings.Instance.BgmVolume.max;
                }
                return;
            }

            source.volume = AudioSettings.Instance.BgmVolume.max;
        }

        /// <summary>
        /// Stop
        /// </summary>
        public async UniTask Stop(BgmKeys key, float fadeDuration = 0.5f)
        {
            if (AudioSettings.Instance == null)
            {
                return;
            }

            AudioSource currentSource = _bgmAudioSources[_currentBgmIndex];

            if (currentSource.clip == AudioSettings.Instance.BgmClipList[key] && currentSource.isPlaying)
            {
                if (fadeDuration > 0)
                {
                    await FadeOut(currentSource, fadeDuration, _bgmCtsHelper.Token);
                }
                currentSource.Stop();
            }
        }

        /// <summary>
        /// AllBgmStop
        /// </summary>
        public async UniTask AllBgmStop(float fadeDuration = 0.5f)
        {
            foreach (BgmKeys key in AudioSettings.Instance.BgmClipList.Keys)
            {
                await Stop(key);
            }
        }

        /// <summary>
        /// Play
        /// </summary>
        public string Play(SeKeys key)
        {
            string tag = string.Empty;

            if (AudioSettings.Instance == null)
            {
                return tag;
            }

            Setup();

            if (key == SeKeys.None)
            {
                return tag;
            }
            if (AudioSettings.Instance.SeClipList.ContainsKey(key))
            {
                AudioSource audioSource = null;
                CancellationTokenHelper ctsHelper = new CancellationTokenHelper();

                if (_seAudioSourcePool.Count > 0)
                {
                    audioSource = _seAudioSourcePool.Dequeue();
                }
                else
                {
                    (string tag, SeKeys key, AudioSource audioSource, CancellationTokenHelper oldCtsHelper) seActiveAudioSource = _seActiveAudioSources.OrderBy(source => source.Item3.clip.length - source.Item3.time).FirstOrDefault();
                    _seActiveAudioSources.Remove(seActiveAudioSource);
                    seActiveAudioSource.oldCtsHelper.Dispose();
                    seActiveAudioSource.audioSource.Stop();

                    audioSource = seActiveAudioSource.audioSource;
                }

                tag = Guid.NewGuid().ToString();

                AudioVolume volume = GetVolume(key);

                audioSource.clip = AudioSettings.Instance.SeClipList[key];
                audioSource.volume = volume.max;
                audioSource.Play();
                ReturnToPoolAfterPlay(key, audioSource, ctsHelper.Token).Forget();

                _seActiveAudioSources.Add((tag, key, audioSource, ctsHelper));

                AdjustVolume(_seActiveAudioSources);

                return tag;
            }

            return tag;
        }

        /// <summary>
        /// SEのボリュームを取得
        /// </summary>
        private AudioVolume GetVolume(SeKeys key)
        {
            if (AudioSettings.Instance.SeVolumeOverride != null && AudioSettings.Instance.SeVolumeOverride.ContainsKey(key))
            {
                return AudioSettings.Instance.SeVolumeOverride[key];
            }
            return AudioSettings.Instance.SeVolume;
        }

        /// <summary>
        /// 再生終了時にプールに戻す
        /// </summary>
        private async UniTaskVoid ReturnToPoolAfterPlay(SeKeys key, AudioSource audioSource, CancellationToken token)
        {
            await UniTask.WaitWhile(() => audioSource.isPlaying, cancellationToken: token);
            _seActiveAudioSources.RemoveAll(_ => _.Item3 == audioSource);
            _seAudioSourcePool.Enqueue(audioSource);
        }

        /// <summary>
        /// SeStopTag
        /// </summary>
        public void SeStopTag(string tag)
        {
            if (AudioSettings.Instance == null)
            {
                return;
            }

            if (tag == string.Empty)
            {
                return;
            }

            if (_seActiveAudioSources == null)
            {
                return;
            }

            foreach ((string tag, SeKeys key, AudioSource audioSource, CancellationTokenHelper ctsHelper) seActiveAudioSource in _seActiveAudioSources)
            {
                if (tag == seActiveAudioSource.tag && seActiveAudioSource.audioSource.isPlaying)
                {
                    Stop(seActiveAudioSource);
                    break;
                }
            }
        }

        /// <summary>
        /// Stop
        /// </summary>
        public void Stop(SeKeys key)
        {
            if (AudioSettings.Instance == null)
            {
                return;
            }

            if (key == SeKeys.None)
            {
                return;
            }

            if (_seActiveAudioSources == null)
            {
                return;
            }

            foreach ((string tag, SeKeys key, AudioSource audioSource, CancellationTokenHelper ctsHelper) seActiveAudioSource in _seActiveAudioSources)
            {
                if (seActiveAudioSource.audioSource.clip == AudioSettings.Instance.SeClipList[key] && seActiveAudioSource.audioSource.isPlaying)
                {
                    Stop(seActiveAudioSource);
                    break;
                }
            }
        }

        /// <summary>
        /// AllSeStop
        /// </summary>
        public void AllSeStop()
        {
            if (_seActiveAudioSources == null)
            {
                return;
            }

            foreach ((string tag, SeKeys key, AudioSource audioSource, CancellationTokenHelper ctsHelper) seActiveAudioSource in _seActiveAudioSources)
            {
                if (seActiveAudioSource.audioSource.isPlaying)
                {
                    Stop(seActiveAudioSource);
                }
            }
            _seActiveAudioSources.Clear();
        }

        /// <summary>
        /// Stop
        /// </summary>
        private void Stop((string tag, SeKeys key, AudioSource audioSource, CancellationTokenHelper ctsHelper) seActiveAudioSource)
        {
            seActiveAudioSource.audioSource.Stop();
            _seAudioSourcePool.Enqueue(seActiveAudioSource.audioSource);
            _seActiveAudioSources.Remove(seActiveAudioSource);
        }

        /// <summary>
        /// Seのボリューム調整
        /// </summary>
        private void AdjustVolume(List<(string tag, SeKeys key, AudioSource audioSource, CancellationTokenHelper ctsHelper)> targets)
        {
            var groupedTargets = targets.GroupBy(target => target.key);

            foreach (var group in groupedTargets)
            {
                AudioVolume seVolume = GetVolume(group.Key);
                float maxTotalVolume = seVolume.totalMax;

                float totalVolume = group.Sum(target => target.audioSource.volume);

                if (totalVolume > maxTotalVolume)
                {
                    float adjustmentFactor = maxTotalVolume / totalVolume;
                    foreach (var target in group)
                    {
                        target.audioSource.volume *= adjustmentFactor;
                    }
                }
            }
        }

        /// <summary>
        /// Play
        /// </summary>
        public string Play(JingleKeys key)
        {
            string tag = string.Empty;

            if (AudioSettings.Instance == null)
            {
                return tag;
            }

            Setup();

            if (key == JingleKeys.None)
            {
                return tag;
            }
            if (AudioSettings.Instance.JingleClipList.ContainsKey(key))
            {
                AudioSource audioSource = null;
                CancellationTokenHelper ctsHelper = new CancellationTokenHelper();

                if (_jingleAudioSourcePool.Count > 0)
                {
                    audioSource = _jingleAudioSourcePool.Dequeue();
                }
                else
                {
                    (string tag, JingleKeys key, AudioSource audioSource, CancellationTokenHelper oldCtsHelper) jingleActiveAudioSource = _jingleActiveAudioSources.FirstOrDefault();
                    _jingleActiveAudioSources.Remove(jingleActiveAudioSource);
                    jingleActiveAudioSource.oldCtsHelper.Dispose();
                    jingleActiveAudioSource.audioSource.Stop();

                    audioSource = jingleActiveAudioSource.audioSource;
                }

                tag = Guid.NewGuid().ToString();

                AudioVolume volume = GetVolume(key);

                audioSource.clip = AudioSettings.Instance.JingleClipList[key];
                audioSource.volume = volume.max;
                audioSource.Play();
                ReturnToPoolAfterPlay(key, audioSource, ctsHelper.Token).Forget();

                _jingleActiveAudioSources.Add((tag, key, audioSource, ctsHelper));

                AdjustVolume(_jingleActiveAudioSources);

                return tag;
            }

            return tag;
        }

        /// <summary>
        /// Jingleのボリュームを取得
        /// </summary>
        private AudioVolume GetVolume(JingleKeys key)
        {
            if (AudioSettings.Instance.JingleVolumeOverride != null && AudioSettings.Instance.JingleVolumeOverride.ContainsKey(key))
            {
                return AudioSettings.Instance.JingleVolumeOverride[key];
            }
            return AudioSettings.Instance.JingleVolume;
        }

        /// <summary>
        /// 再生終了時にプールに戻す
        /// </summary>
        private async UniTaskVoid ReturnToPoolAfterPlay(JingleKeys key, AudioSource audioSource, CancellationToken token)
        {
            await UniTask.WaitWhile(() => audioSource.isPlaying, cancellationToken: token);
            _jingleActiveAudioSources.RemoveAll(_ => _.Item3 == audioSource);
            _jingleAudioSourcePool.Enqueue(audioSource);
        }

        /// <summary>
        /// JingleStopTag
        /// </summary>
        public void JingleStopTag(string tag)
        {
            if (AudioSettings.Instance == null)
            {
                return;
            }

            if (tag == string.Empty)
            {
                return;
            }

            if (_jingleActiveAudioSources == null)
            {
                return;
            }

            foreach ((string tag, JingleKeys key, AudioSource audioSource, CancellationTokenHelper ctsHelper) jingleActiveAudioSource in _jingleActiveAudioSources)
            {
                if (tag == jingleActiveAudioSource.tag && jingleActiveAudioSource.audioSource.isPlaying)
                {
                    Stop(jingleActiveAudioSource);
                    break;
                }
            }
        }

        /// <summary>
        /// Stop
        /// </summary>
        public void Stop(JingleKeys key)
        {
            if (AudioSettings.Instance == null)
            {
                return;
            }

            if (key == JingleKeys.None)
            {
                return;
            }

            if (_jingleActiveAudioSources == null)
            {
                return;
            }

            foreach ((string tag, JingleKeys key, AudioSource audioSource, CancellationTokenHelper ctsHelper) jingleActiveAudioSource in _jingleActiveAudioSources)
            {
                if (jingleActiveAudioSource.audioSource.clip == AudioSettings.Instance.JingleClipList[key] && jingleActiveAudioSource.audioSource.isPlaying)
                {
                    Stop(jingleActiveAudioSource);
                    break;
                }
            }
        }

        /// <summary>
        /// AllJingleStop
        /// </summary>
        public void AllJingleStop()
        {
            if (_jingleActiveAudioSources == null)
            {
                return;
            }

            foreach ((string tag, JingleKeys key, AudioSource audioSource, CancellationTokenHelper ctsHelper) jingleActiveAudioSource in _jingleActiveAudioSources)
            {
                if (jingleActiveAudioSource.audioSource.isPlaying)
                {
                    Stop(jingleActiveAudioSource);
                }
            }
            _jingleActiveAudioSources.Clear();
        }

        /// <summary>
        /// Stop
        /// </summary>
        private void Stop((string tag, JingleKeys key, AudioSource audioSource, CancellationTokenHelper ctsHelper) jingleActiveAudioSource)
        {
            jingleActiveAudioSource.ctsHelper.Dispose();
            jingleActiveAudioSource.audioSource.Stop();
            _jingleAudioSourcePool.Enqueue(jingleActiveAudioSource.audioSource);
            _jingleActiveAudioSources.Remove(jingleActiveAudioSource);
        }

        /// <summary>
        /// Jingleのボリューム調整
        /// </summary>
        private void AdjustVolume(List<(string tag, JingleKeys key, AudioSource audioSource, CancellationTokenHelper ctsHelper)> targets)
        {
            var groupedTargets = targets.GroupBy(target => target.key);

            foreach (var group in groupedTargets)
            {
                AudioVolume volume = GetVolume(group.Key);
                float maxTotalVolume = volume.totalMax;

                float totalVolume = group.Sum(target => target.audioSource.volume);

                if (totalVolume > maxTotalVolume)
                {
                    float adjustmentFactor = maxTotalVolume / totalVolume;
                    foreach (var target in group)
                    {
                        target.audioSource.volume *= adjustmentFactor;
                    }
                }
            }
        }

        /// <summary>
        /// Play
        /// </summary>
        public string Play(VoiceKeys key)
        {
            string tag = string.Empty;

            if (AudioSettings.Instance == null)
            {
                return tag;
            }

            Setup();

            if (key == VoiceKeys.None)
            {
                return tag;
            }
            if (AudioSettings.Instance.VoiceClipList.ContainsKey(key))
            {
                AudioSource audioSource = null;
                CancellationTokenHelper ctsHelper = new CancellationTokenHelper();

                if (_voiceAudioSourcePool.Count > 0)
                {
                    audioSource = _voiceAudioSourcePool.Dequeue();
                }
                else
                {
                    (string tag, VoiceKeys key, AudioSource audioSource, CancellationTokenHelper oldCtsHelper) voiceActiveAudioSource = _voiceActiveAudioSources.OrderBy(source => source.Item3.clip.length - source.Item3.time).FirstOrDefault();
                    _voiceActiveAudioSources.Remove(voiceActiveAudioSource);
                    voiceActiveAudioSource.oldCtsHelper.Dispose();
                    voiceActiveAudioSource.audioSource.Stop();

                    audioSource = voiceActiveAudioSource.audioSource;
                }

                tag = Guid.NewGuid().ToString();

                AudioVolume volume = GetVolume(key);

                audioSource.clip = AudioSettings.Instance.VoiceClipList[key];
                audioSource.volume = volume.max;
                audioSource.Play();
                ReturnToPoolAfterPlay(key, audioSource, ctsHelper.Token).Forget();

                _voiceActiveAudioSources.Add((tag, key, audioSource, ctsHelper));

                AdjustVolume(_voiceActiveAudioSources);

                return tag;
            }

            return tag;
        }

        /// <summary>
        /// Voiceのボリュームを取得
        /// </summary>
        private AudioVolume GetVolume(VoiceKeys key)
        {
            if (AudioSettings.Instance.VoiceVolumeOverride != null && AudioSettings.Instance.VoiceVolumeOverride.ContainsKey(key))
            {
                return AudioSettings.Instance.VoiceVolumeOverride[key];
            }
            return AudioSettings.Instance.VoiceVolume;
        }

        /// <summary>
        /// 再生終了時にプールに戻す
        /// </summary>
        private async UniTaskVoid ReturnToPoolAfterPlay(VoiceKeys key, AudioSource audioSource, CancellationToken token)
        {
            await UniTask.WaitWhile(() => audioSource.isPlaying, cancellationToken: token);
            _voiceActiveAudioSources.RemoveAll(_ => _.Item3 == audioSource);
            _voiceAudioSourcePool.Enqueue(audioSource);
        }

        /// <summary>
        /// VoiceStopTag
        /// </summary>
        public void VoiceStopTag(string tag)
        {
            if (AudioSettings.Instance == null)
            {
                return;
            }

            if (tag == string.Empty)
            {
                return;
            }

            if (_voiceActiveAudioSources == null)
            {
                return;
            }

            foreach ((string tag, VoiceKeys key, AudioSource audioSource, CancellationTokenHelper ctsHelper) voiceActiveAudioSource in _voiceActiveAudioSources)
            {
                if (tag == voiceActiveAudioSource.tag && voiceActiveAudioSource.audioSource.isPlaying)
                {
                    Stop(voiceActiveAudioSource);
                    break;
                }
            }
        }

        /// <summary>
        /// Stop
        /// </summary>
        public void Stop(VoiceKeys key)
        {
            if (AudioSettings.Instance == null)
            {
                return;
            }

            if (key == VoiceKeys.None)
            {
                return;
            }

            if (_voiceActiveAudioSources == null)
            {
                return;
            }

            foreach ((string tag, VoiceKeys key, AudioSource audioSource, CancellationTokenHelper ctsHelper) voiceActiveAudioSource in _voiceActiveAudioSources)
            {
                if (voiceActiveAudioSource.audioSource.clip == AudioSettings.Instance.VoiceClipList[key] && voiceActiveAudioSource.audioSource.isPlaying)
                {
                    Stop(voiceActiveAudioSource);
                    break;
                }
            }
        }

        /// <summary>
        /// AllVoiceStop
        /// </summary>
        public void AllVoiceStop()
        {
            if (_voiceActiveAudioSources == null)
            {
                return;
            }

            foreach ((string tag, VoiceKeys key, AudioSource audioSource, CancellationTokenHelper ctsHelper) voiceActiveAudioSource in _voiceActiveAudioSources)
            {
                if (voiceActiveAudioSource.audioSource.isPlaying)
                {
                    Stop(voiceActiveAudioSource);
                }
            }
            _voiceActiveAudioSources.Clear();
        }

        /// <summary>
        /// Stop
        /// </summary>
        private void Stop((string tag, VoiceKeys key, AudioSource audioSource, CancellationTokenHelper ctsHelper) voiceActiveAudioSource)
        {
            voiceActiveAudioSource.audioSource.Stop();
            _voiceAudioSourcePool.Enqueue(voiceActiveAudioSource.audioSource);
            _voiceActiveAudioSources.Remove(voiceActiveAudioSource);
        }

        /// <summary>
        /// Seのボリューム調整
        /// </summary>
        private void AdjustVolume(List<(string tag, VoiceKeys key, AudioSource audioSource, CancellationTokenHelper ctsHelper)> targets)
        {
            var groupedTargets = targets.GroupBy(target => target.key);

            foreach (var group in groupedTargets)
            {
                AudioVolume volume = GetVolume(group.Key);
                float maxTotalVolume = volume.totalMax;

                float totalVolume = group.Sum(target => target.audioSource.volume);

                if (totalVolume > maxTotalVolume)
                {
                    float adjustmentFactor = maxTotalVolume / totalVolume;
                    foreach (var target in group)
                    {
                        target.audioSource.volume *= adjustmentFactor;
                    }
                }
            }
        }
    }
}
オーディオ設定(Singleton)
AudioSettings.cs
using System.Collections.Generic;
using UnityEngine;
using Utils; // 詳細は後述してます

namespace AudioSystem
{
    sealed public class AudioSettings : SingletonMonoBehaviour<AudioSettings>
    {
        [Header("BGMの音量")]

        [SerializeField]
        private AudioVolume _bgmVolume = null;
        public AudioVolume BgmVolume => _bgmVolume;

        [Space]
        [Header("SEの音量")]

        [SerializeField]
        private AudioVolume _seVolume = null;
        public AudioVolume SeVolume => _seVolume;

        [SerializeField]
        private SerializableDictionary<SeKeys, AudioVolume> _seVolumeOverride = null;
        public SerializableDictionary<SeKeys, AudioVolume> SeVolumeOverride => _seVolumeOverride;

        [Space]
        [Header("Jingleの音量")]

        [SerializeField]
        private AudioVolume _jingleVolume = null;
        public AudioVolume JingleVolume => _jingleVolume;

        [SerializeField]
        private SerializableDictionary<JingleKeys, AudioVolume> _jingleVolumeOverride = null;
        public SerializableDictionary<JingleKeys, AudioVolume> JingleVolumeOverride => _jingleVolumeOverride;

        [Space]
        [Header("Voiceの音量")]

        [SerializeField]
        private AudioVolume _voiceVolume = null;
        public AudioVolume VoiceVolume => _voiceVolume;

        [SerializeField]
        private SerializableDictionary<VoiceKeys, AudioVolume> _voiceVolumeOverride = null;
        public SerializableDictionary<VoiceKeys, AudioVolume> VoiceVolumeOverride => _voiceVolumeOverride;

        [Space]
        [Header("同時再生可能数")]

        [SerializeField]
        private int _bgmAudioSourcePoolSize = 2;
        public int BgmAudioSourcePoolSize => _bgmAudioSourcePoolSize;

        [SerializeField]
        private int _seAudioSourcePoolSize = 30;
        public int SeAudioSourcePoolSize => _seAudioSourcePoolSize;

        [SerializeField]
        private int _jingleAudioSourcePoolSize = 5;
        public int JingleAudioSourcePoolSize => _jingleAudioSourcePoolSize;

        [SerializeField]
        private int _voiceAudioSourcePoolSize = 5;
        public int VoiceAudioSourcePoolSize => _voiceAudioSourcePoolSize;

        [Space]
        [Header("クリップとキーの紐づけ")]

        [SerializeField]
        private SerializableDictionary<BgmKeys, AudioClip> _bgmClipList = null;
        public Dictionary<BgmKeys, AudioClip> BgmClipList => _bgmClipList;

        [SerializeField]
        private SerializableDictionary<SeKeys, AudioClip> _seClipList = null;
        public Dictionary<SeKeys, AudioClip> SeClipList => _seClipList;

        [SerializeField]
        private SerializableDictionary<JingleKeys, AudioClip> _jingleClipList = null;
        public Dictionary<JingleKeys, AudioClip> JingleClipList => _jingleClipList;

        [SerializeField]
        private SerializableDictionary<VoiceKeys, AudioClip> _voiceClipList = null;
        public Dictionary<VoiceKeys, AudioClip> VoiceClipList => _voiceClipList;
    }
}
オーディオ音量
AudioVolume.cs
using System;
using UnityEngine;

namespace AudioSystem
{
    [Serializable]
    public class AudioVolume
    {
        [Range(0.0f, 1.0f)]
        public float min = 0.0f;

        [Range(0.0f, 1.0f)]
        public float max = 1.0f;

        [Range(0.0f, 1.0f)]
        public float fadeRate = 0.01f;

        [Range(0.0f, 10.0f)]
        public float totalMax = 1.0f;
    }
}
オーディオKeys
AudioKeys.cs
namespace AudioSystem
{
    public enum BgmKeys
    {
        None,
    }

    public enum SeKeys
    {
        None,
    }

    public enum JingleKeys
    {
        None,
    }

    public enum VoiceKeys
    {
        None,
    }
}

再生スクリプト

BGM再生
PlayBgm.cs
using UnityEngine;

namespace AudioSystem
{
    public class PlayBgm : MonoBehaviour
    {
        [SerializeField]
        private BgmKeys _key = BgmKeys.None;

        [SerializeField]
        private bool _isSwitching = false;

        [SerializeField]
        private float _fadeDuration = 0.5f;

        private BgmKeys _prevKey = BgmKeys.None;

        /// <summary>
        /// アクティブになった時
        /// </summary>
        private async void OnEnable()
        {
            if (AudioManager.Instance != null)
            {
                if (_isSwitching)
                {
                    _prevKey = AudioManager.Instance.GetCurrentBgm();
                }
                await AudioManager.Instance.Play(_key, _fadeDuration);
            }
        }

        /// <summary>
        /// 破壊時
        /// </summary>
        private async void OnDestroy()
        {
            if (AudioManager.Instance != null)
            {
                if (_isSwitching && _prevKey != BgmKeys.None)
                {
                    await AudioManager.Instance.Play(_prevKey, _fadeDuration);
                }
            }
        }
    }
}
SE再生
PlaySe.cs
using Cysharp.Threading.Tasks;
using System;
using UnityEngine;

namespace AudioSystem
{
    public class PlaySe : MonoBehaviour
    {
        [SerializeField]
        private SeKeys _key = SeKeys.None;

        [SerializeField]
        private float _waitTime = 0.0f;

        [SerializeField]
        private bool _isEndPlayback = false;

        [SerializeField, ReadOnly]
        private string _tag = string.Empty;

        /// <summary>
        /// アクティブになった時
        /// </summary>
        private async void OnEnable()
        {
            if (_waitTime > 0.0f)
            {
                await UniTask.Delay(TimeSpan.FromSeconds(_waitTime));
            }
            _tag = AudioManager.Instance?.Play(_key);
        }

        /// <summary>
        /// アクティブになった時
        /// </summary>
        private void OnDestroy()
        {
            if (_isEndPlayback)
            {
                return;
            }
            AudioManager.Instance?.SeStopTag(_tag);
        }
    }
}
Jingle再生
PlayJingle.cs
using Cysharp.Threading.Tasks;
using System;
using UnityEngine;

namespace AudioSystem
{
    public class PlayJingle : MonoBehaviour
    {
        [SerializeField]
        private JingleKeys _key = JingleKeys.None;

        [SerializeField]
        private float _waitTime = 0.0f;

        [SerializeField]
        private bool _isEndPlayback = false;

        [SerializeField, ReadOnly]
        private string _tag = string.Empty;

        /// <summary>
        /// アクティブになった時
        /// </summary>
        private async void OnEnable()
        {
            if (_waitTime > 0.0f)
            {
                await UniTask.Delay(TimeSpan.FromSeconds(_waitTime));
            }
            _tag = AudioManager.Instance.Play(_key);
        }

        /// <summary>
        /// アクティブになった時
        /// </summary>
        private void OnDestroy()
        {
            if (_isEndPlayback)
            {
                return;
            }
            AudioManager.Instance?.JingleStopTag(_tag);
        }
    }
}
Voice再生
PlayVoice.cs
using Cysharp.Threading.Tasks;
using System;
using UnityEngine;

namespace AudioSystem
{
    public class PlayVoice : MonoBehaviour
    {
        [SerializeField]
        private VoiceKeys _key = VoiceKeys.None;

        [SerializeField]
        private float _waitTime = 0.0f;

        [SerializeField]
        private bool _isEndPlayback = true;

        [SerializeField, ReadOnly]
        private string _tag = string.Empty;

        /// <summary>
        /// アクティブになった時
        /// </summary>
        private async void OnEnable()
        {
            if (_waitTime > 0.0f)
            {
                await UniTask.Delay(TimeSpan.FromSeconds(_waitTime));
            }
            _tag = AudioManager.Instance?.Play(_key);
        }

        /// <summary>
        /// アクティブになった時
        /// </summary>
        private async void OnDestroy()
        {
            if (_isEndPlayback)
            {
                return;
            }
            AudioManager.Instance?.VoiceStopTag(_tag);
        }
    }
}

Utilsについて

以下の機能を共通処理としてUtils名前空間に定義してます

・GetOrAddComponent
以下のようなEX処理を定義してます。

        /// <summary>
        /// コンポーネント取得/追加
        /// </summary>
        static public T GetOrAddComponent<T>(this GameObject obj) where T : Component
        {
            T component = obj.GetComponent<T>();
            if (component != null)
            {
                return component;
            }
            return obj.AddComponent<T>();
        }

・CancellationTokenHelper
こちらの記事で紹介している、UniTaskのキャンセル処理を使用しています。
https://zenn.dev/tmb/articles/e4fb3fe350852f
・SingletonMonoBehaviour
https://zenn.dev/tmb/articles/e6864cd669d46d

Discussion