Files
Shopkeeper/Assets/SimpleAudioManager/Scripts/Manager.cs
Caleb Sandford deQuincey e9c9bc4409 initial game submission
2025-06-11 13:04:35 +01:00

293 lines
11 KiB
C#

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Audio;
namespace SimpleAudioManager
{
public class Manager : MonoBehaviour
{
#region PROPERTIES
/// <summary>
/// Singleton
/// </summary>
public static Manager instance => _instance;
private static Manager _instance = null;
/// <summary>
/// The attached audio source
/// </summary>
[Header("CONFIGURATIONS")]
[Tooltip("The audio source prefab which will be used in the audio source pool.")] public GameObject audioSourcePrefab = null;
private List<AudioSource> sourcePool = new List<AudioSource>();
private int _currentSourceIndex = -1;
private AudioSource _currentSource => (_currentSourceIndex == -1 || sourcePool == null || sourcePool.Count <= 0 || sourcePool.Count < _currentSourceIndex) ? null : sourcePool[_currentSourceIndex];
public AudioMixer audioMixer = null;
public AudioMixerGroup musicMixerGroup = null;
[Tooltip("Should the current song loop?")] public bool loopCurrentSong = true;
private Coroutine _loop;
private Song.Data _currentSongData;
private int _currentSongIndex = 0;
private int _currentIntensityIndex = 0;
/// <summary>
/// The time before either a non-looping clip ends or the next loop of a looping clip begins
/// </summary>
public float clipTimeRemaining => (_nextLoopStartTime != 0f) ? ((float)(_nextLoopStartTime - AudioSettings.dspTime) + ((!loopCurrentSong)? _currentSongData.reverbTail : 0f)) : 0f;
private double _nextLoopStartTime = 0;
[Tooltip("Should the manager play the first song on awake?")] public bool playOnAwake = true;
[Tooltip("The maximum volume for the audio clips.")][Range(0f, 1f)] public float maxVolume = 1f;
[Tooltip("The amount of time it will take for different songs to blend between one-another.")] public float defaultSongBlendDuration = 1f;
[Tooltip("The amount of time it will take for different intensities of the same song to blend between one-another.")] public float defaultIntensityBlendDuration = 1f;
[Space(8f)]
/// <summary>
/// The available songs for the manager
/// </summary>
[Tooltip(
"The list of available songs for the manager to play.\n" +
"-To create a new song:\n" +
" -Right Click and select:\n" +
" Create->SimpleAudioManager->Song\n" +
" -Add the intensity clips or any other desired clips\n" +
" -Set the reverb tail time\n" +
" (Seconds before the end of a clip to loop it)\n" +
" (Shown in parentheses on Ovani Folders)\n" +
" -Drag & Drop your songs onto this list")] [SerializeField] private List<Song> _songs = new List<Song>();
private List<Song.Data> _data = new List<Song.Data>();
#endregion
#region PUBLIC METHODS
/// <summary>
/// Shorthand set Intensity
/// </summary>
public void SetIntensity(int pIntensity) => SetIntensity(pIntensity, defaultIntensityBlendDuration, defaultIntensityBlendDuration);
/// <summary>
/// Sets the intensity for the current song
/// </summary>
public void SetIntensity(int pIntensity, float pBlendOutDuration, float pBlendInDuration)
{
if (_currentSongData.intensityClips.Count > Mathf.Max(pIntensity, 0))
{
PlaySong( new PlaySongOptions()
{
song = _currentSongIndex,
intensity = Mathf.Max(pIntensity, 0),
startTime = sourcePool[_currentSourceIndex].time,
blendOutTime = pBlendOutDuration,
blendInTime = pBlendInDuration
});
}
}
/// <summary>
/// Plays the specified song and attempts to match the current intensity
/// </summary>
public void PlaySong(int pSong) => PlaySong( new PlaySongOptions() {
song = pSong,
intensity = _currentIntensityIndex,
blendOutTime = defaultSongBlendDuration,
blendInTime = defaultSongBlendDuration
});
/// <summary>
/// Plays the specified song
/// </summary>
public void PlaySong(PlaySongOptions pOptions)
{
// Updates the data collection
_UpdateSongData();
// Confirm song exists, clip exists
if (_data == null || _data.Count == 0 || _data.Count <= pOptions.song) return;
if (_data[pOptions.song].intensityClips == null || _data[pOptions.song].intensityClips.Count == 0) return;
// Do our best to match intensity
if (_data[pOptions.song].intensityClips.Count <= pOptions.intensity) pOptions.intensity = _data[pOptions.song].intensityClips.Count - 1;
// Get the next available audio source
if (_currentSourceIndex != -1)
{
AudioSource _current = _currentSource;
// Passing in -1 blendOutTime will let the source play the audio out until the end of the track at its current volume
float _endVolume = (pOptions.blendOutTime == -1f) ? _current.volume : 0f;
float _fadeTime = (pOptions.blendOutTime == -1f) ? _currentSongData.reverbTail : pOptions.blendOutTime;
StartCoroutine(_FadeVolume(_current, _current.volume, _endVolume, _fadeTime));
}
// Change the current source to the next available source
_currentSourceIndex = _GetNextSourceIndex();
AudioSource _nextSource = _currentSource;
// Apply the provided options
_currentSongIndex = pOptions.song;
_currentIntensityIndex = pOptions.intensity;
_currentSongData = _data[_currentSongIndex];
AudioClip _clip = _currentSongData.intensityClips[_currentIntensityIndex];
_nextSource.gameObject.name = _clip.name;
// Kill the previous loop and start a new loop routine with the updated song information
if (_loop != null) StopCoroutine(_loop);
_loop = StartCoroutine(_Loop(pOptions.startTime));
StartCoroutine(_FadeVolume(_nextSource, 0f, maxVolume, pOptions.blendInTime));
_nextSource.clip = _clip;
_nextSource.time = pOptions.startTime;
_nextSource.Play();
}
/// <summary>
/// Play song options
/// </summary>
public struct PlaySongOptions
{
public int song;
public int intensity;
public float startTime;
public float blendOutTime;
public float blendInTime;
}
/// <summary>
/// Stops the current song playing
/// </summary>
public void StopSong(float pFadeOutDuration)
{
AudioSource _current = _currentSource;
StartCoroutine(_FadeVolume(_current, _current.volume, 0f, pFadeOutDuration));
}
#endregion
#region PRIVATE METHODS
/// <summary>
/// Config
/// </summary>
private void Awake()
{
_instance = _instance ?? this;
if (_instance != this)
{
DestroyImmediate(gameObject);
return;
}
if (playOnAwake) StartCoroutine(_delay());
IEnumerator _delay()
{
yield return new WaitForSecondsRealtime(0.25f);
PlaySong(0);
}
}
/// <summary>
/// Clear out the pseudo-singleton
/// </summary>
private void OnDestroy() => _instance = _instance == this ? null : _instance;
/// <summary>
/// Builds the data pool for the songs
/// </summary>
private void _UpdateSongData()
{
_data.Clear();
_songs.ForEach(s => _data.Add(s.ToSongData()));
}
/// <summary>
/// Gets the next available source that is not playing
/// </summary>
private int _GetNextSourceIndex()
{
AudioSource next = sourcePool.Find(s => !s.isPlaying);
if (next == null)
{
next = Instantiate(audioSourcePrefab, transform).GetComponent<AudioSource>();
// Assign the mixer group here:
if (musicMixerGroup != null)
next.outputAudioMixerGroup = musicMixerGroup;
sourcePool.Add(next);
}
next.gameObject.SetActive(true);
return sourcePool.IndexOf(next);
}
/// <summary>
/// Force an audio source to fade in or out
/// </summary>
private IEnumerator _FadeVolume(AudioSource pSource, float pStart, float pEnd, float pDuration)
{
pDuration = Mathf.Max(pDuration, 0f);
float duration = 0f;
// Perform volume fade
while (duration < pDuration)
{
yield return new WaitForEndOfFrame();
duration += Time.unscaledDeltaTime;
pSource.volume = Mathf.SmoothStep(pStart, pEnd, duration / pDuration);
}
// Ensure volume is at desired volume
pSource.volume = pEnd;
// If the volume is 0 or
// The fade was between the same values (this is used when looping to allow for the previous clip to play out without fading)
if (pSource.volume == 0f || pEnd == pStart)
{
// If the fade is for the current audio source
if (pSource == _currentSource)
{
sourcePool.ForEach(s => s.Stop());
StopCoroutine(_loop);
_currentSourceIndex = -1;
_nextLoopStartTime = 0f;
}
pSource.volume = 0f;
pSource.Stop();
pSource.gameObject.SetActive(false);
}
}
/// <summary>
/// Plays the provided song after a wait period
/// </summary>
private IEnumerator _Loop(float pStartTime)
{
float _fullLength = _currentSongData.intensityClips[_currentIntensityIndex].length;
float _waitTime = _fullLength - _currentSongData.reverbTail - pStartTime;
_nextLoopStartTime = AudioSettings.dspTime + _waitTime;
yield return new WaitForSecondsRealtime(_waitTime);
if (!loopCurrentSong)
{
// Queue the current audio source to play out for the remainder of the duration
AudioSource _current = _currentSource;
StartCoroutine(_FadeVolume(_current, _current.volume, _current.volume, _currentSongData.reverbTail));
yield break;
}
// If looping, play the song
PlaySong( new PlaySongOptions() {
song = _currentSongIndex,
intensity = _currentIntensityIndex,
blendOutTime = -1f,
blendInTime = 0.01f
});
}
#endregion
}
}
/*
*
* Written by Ovani Sound & Brutiful Games
* No credit required.
* Revision: 06/22/2023
*
*/