You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

924 lines
41 KiB

using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Audio;
using UnityEngine.Events;
namespace E7.Introloop
{
/// <summary>
/// A component that coordinates 4 <see cref="AudioSource"/> together with scheduling methods
/// to achieve gap-less looping music with intro section.
/// </summary>
/// <remarks>
/// <para>
/// 2 <see cref="AudioSource"/> uses scheduling methods to stitch up audio precisely, while the other 2 sources
/// are there to support cross fading to a new Introloop audio.
/// </para>
/// <para>
/// Potentially there is a moment when all 4 sources are playing at the same time.
/// (e.g. One introlooping audio at the seam, while being tasked to cross fade into
/// an another introloop audio that starts near the seam.)
/// </para>
/// </remarks>
public class IntroloopPlayer : MonoBehaviour
{
/// <summary>
/// This fade is inaudible, it helps removing loud pop/click when you stop song suddenly.
/// This is used automatically when you <see cref="Stop()"/>.
/// If you really don't want this, use <see cref="Stop(float)"/> with 0 second fade length.
/// </summary>
private const float popRemovalFadeTime = 0.055f;
private static IntroloopPlayer instance;
private static AudioSource singletonInstanceTemplateSource;
[Tooltip("This works the same as Clips slot on the AudioSource. It is the asset to play when you call Play() " +
"in the API. However, Introloop also has Play overload where you can just send an asset to play via " +
"the arguments regardless of asset reference connected to this field.")]
[SerializeField] private IntroloopAudio defaultIntroloopAudio;
[Tooltip("Introloop spawns 4 AudioSource at runtime to manage the loops. All sources inherits settings from " +
"this template AudioSource, including the output AudioMixerGroup. You can add the AudioSource to be " +
"the template next to this IntroloopPlayer component then connect to this slot.\n\n" +
"If it is not assigned, new AudioSource component will be added next to " +
"this component on the first play with settings : Priority 0, Spatial Blend 2D. (Expected settings " +
"for background music uses.) Then this field will reference that newly added AudioSource as a template.")]
[SerializeField] private AudioSource templateSource;
[Tooltip("Works like Play On Awake of AudioSource, play the connected \"Default Introloop Audio\" asset " +
"automatically on Awake().")]
[SerializeField] private bool playOnAwake;
[SerializeField] private IntroloopSettings introloopSettings;
private readonly float[] fadeLength = new float[2];
private readonly float[] towardsVolume = new float[2];
private readonly IntroloopTrack[] twoTracks = new IntroloopTrack[2];
private readonly bool[] willPause = new bool[2];
private readonly bool[] willStop = new bool[2];
/// <summary>
/// It will change to 0 on first <see cref="Play(E7.Introloop.IntroloopAudio,float,float)"/>.
/// 0 is the first track.
/// </summary>
private int currentTrack = 1;
private bool importantChildrenCreated;
private IntroloopAudio previousPlay;
private float timeBeforePause;
/// <summary>
/// Works like <see cref="AudioSource.clip"/> property. You can set this to any <see cref="IntroloopAudio"/>
/// for it to be used when you call <see cref="Play()"/> or <see cref="Play(float,float)"/> next.
/// </summary>
public IntroloopAudio DefaultIntroloopAudio
{
get => defaultIntroloopAudio;
set => defaultIntroloopAudio = value;
}
/// <summary>
/// Works like <see cref="AudioSource.playOnAwake"/>, play the connected <see cref="DefaultIntroloopAudio"/>
/// automatically on <c>Awake</c>.
/// </summary>
public bool PlayOnAwake
{
get => playOnAwake;
set => playOnAwake = value;
}
/// <summary>
/// <para>
/// When it would spawn 4 <see cref="AudioSource"/> for the first time on <c>Start()</c>, read out the
/// fields from this <see cref="AudioSource"/> reference and copy them to all 4. Assigning this after
/// it had already spawned underlying 4 <see cref="AudioSource"/> has no effect.
/// </para>
/// <para>
/// To apply this template again after <c>Start()</c>, use <see cref="ApplyAudioSource"/>.
/// The argument could be this <see cref="TemplateSource"/> or any other <see cref="AudioSource"/>.
/// </para>
/// </summary>
/// <remarks>
/// The <see cref="AudioSource"/> does not need to be <see cref="MonoBehaviour.enabled"/> since it just
/// need to read the fields out for copy. Also it does not need to be anywhere on the scene,
/// it can come from a prefab with <see cref="AudioSource"/> in your project.
/// </remarks>
public AudioSource TemplateSource
{
get
{
if (templateSource != null)
{
return templateSource;
}
// A fallback to pickup nearby AudioSource as a template.
templateSource = GetComponent<AudioSource>();
if (templateSource != null)
{
return templateSource;
}
// If no template source, make it a high priority + 2D source by default.
templateSource = gameObject.AddComponent<AudioSource>();
SetupDefaultAudioSourceForIntroloop(templateSource);
return templateSource;
}
set => templateSource = value;
}
/// <summary>
/// If you wish to do something that affects all 4 <see cref="AudioSource"/> that Introloop utilize at once,
/// do a <c>foreach</c> on this property.
/// </summary>
/// <remarks>
/// You should not use this in <c>Awake</c>, as Introloop might still
/// not yet spawn the <see cref="AudioSource"/>.
/// </remarks>
public IEnumerable<AudioSource> InternalAudioSources
{
get
{
if (twoTracks == null)
{
throw new IntroloopException(
"Child game objects of Introloop player is not yet initialized. " +
"Please avoid accessing internal AudioSource on Awake.");
}
foreach (var aSource in twoTracks[0].AllAudioSources)
{
yield return aSource;
}
foreach (var aSource in twoTracks[1].AllAudioSources)
{
yield return aSource;
}
}
}
/// <summary>
/// <para>
/// Get a convenient singleton instance of <see cref="IntroloopPlayer"/> from anywhere in your code.
/// It has <c>DontDestroyOnLoad</c> applied.
/// </para>
/// <para>
/// Before calling this <b>for the first time</b>, call <see cref="SetSingletonInstanceTemplateSource"/>
/// first to setup its <see cref="TemplateSource"/> from script. (It does not exist until runtime, you
/// cannot setup the template ahead of time unlike non-singleton instances.)
/// </para>
/// </summary>
public static IntroloopPlayer Instance
{
get
{
if (instance != null)
{
return instance;
}
instance = InstantiateSingletonPlayer<IntroloopPlayer>(singletonInstanceTemplateSource);
instance.name = IntroloopSettings.singletonObjectPrefix + instance.name;
return instance;
}
}
private void Awake()
{
if (introloopSettings == null)
{
introloopSettings = new IntroloopSettings();
}
CreateImportantChildren();
if (playOnAwake)
{
Play();
}
}
private void Start()
{
TemplateSource.enabled = false;
ApplyAudioSource(TemplateSource);
}
private void Update()
{
FadeUpdate();
twoTracks[0].SchedulingUpdate();
twoTracks[1].SchedulingUpdate();
}
/// <summary>
/// <para>
/// This is a dirty workaround for the bug in 2019.1+ where on game minimize or
/// <see cref="AudioListener"/> pause, all <see cref="AudioSource.SetScheduledEndTime(double)"/> will be lost.
/// </para>
/// <para>
/// I confirmed it is not a problem in 2018.4 LTS.
/// The ideal fix is to call Pause just before the game goes to minimize then
/// Resume after we comeback to reschedule.
/// </para>
/// <para>
/// However at this callback Pause does not work, as all audio are already on its way to pausing.
/// So an another approach is that we will remember the time just before the pause, and the play again
/// after coming back using that time. The Seek method can be used instead of Play here so you don't have to specify the
/// previous audio.
/// </para>
/// <para>
/// Please see : https://forum.unity.com/threads/introloop-easily-play-looping-music-with-intro-section-v4-0-0-2019.378370/#post-4793741
/// Track the case here : https://fogbugz.unity3d.com/default.asp?1151637_4i53coq9v07qctp1
/// </para>
/// </summary>
public void OnApplicationPause(bool paused)
{
if (paused)
{
timeBeforePause = GetPlayheadTime();
}
else
{
Seek(timeBeforePause);
}
}
private protected static T InstantiateSingletonPlayer<T>(AudioSource templateOfTemplateSource)
where T : IntroloopPlayer
{
var type = typeof(T);
var typeString = type.Name;
var newIntroloopPlayerObject = new GameObject(typeString);
var playerComponent = newIntroloopPlayerObject.AddComponent<T>();
var templateAudioSource = newIntroloopPlayerObject.AddComponent<AudioSource>();
playerComponent.TemplateSource = templateOfTemplateSource;
SetupDefaultAudioSourceForIntroloop(templateAudioSource);
if (templateOfTemplateSource != null)
{
CopyAudioSourceFields(templateAudioSource, templateOfTemplateSource);
}
DontDestroyOnLoad(newIntroloopPlayerObject);
playerComponent.CreateImportantChildren();
return playerComponent;
}
private void CreateImportantChildren()
{
if (importantChildrenCreated)
{
return;
}
// These are all the components that make this plugin works. Basically 4 AudioSources with special control script
// to juggle music file carefully, stop/pause/resume gracefully while Introloop-ing.
var musicPlayerTransform = transform;
var musicTrack1 = new GameObject();
musicTrack1.AddComponent<IntroloopTrack>();
musicTrack1.name = "IntroloopTrack 1";
musicTrack1.transform.parent = musicPlayerTransform;
musicTrack1.transform.localPosition = Vector3.zero;
twoTracks[0] = musicTrack1.GetComponent<IntroloopTrack>();
twoTracks[0].introloopSettings = introloopSettings;
var musicTrack2 = new GameObject();
musicTrack2.AddComponent<IntroloopTrack>();
musicTrack2.name = "IntroloopTrack 2";
musicTrack2.transform.parent = musicPlayerTransform;
musicTrack2.transform.localPosition = Vector3.zero;
twoTracks[1] = musicTrack2.GetComponent<IntroloopTrack>();
twoTracks[1].introloopSettings = introloopSettings;
importantChildrenCreated = true;
}
private static void SetupDefaultAudioSourceForIntroloop(AudioSource audioSource)
{
audioSource.spatialBlend = 0;
audioSource.priority = 0;
}
private void FadeUpdate()
{
//For two main tracks
for (var i = 0; i < 2; i++)
{
var towardsVolumeBgmVolumeApplied = towardsVolume[i];
if (!(Math.Abs(twoTracks[i].FadeVolume - towardsVolumeBgmVolumeApplied) > 0.0001f))
{
continue;
}
// Handles the fade in/out
if (fadeLength[i] == 0)
{
twoTracks[i].FadeVolume = towardsVolumeBgmVolumeApplied;
}
else
{
if (twoTracks[i].FadeVolume > towardsVolumeBgmVolumeApplied)
{
twoTracks[i].FadeVolume -= Time.unscaledDeltaTime / fadeLength[i];
if (twoTracks[i].FadeVolume <= towardsVolumeBgmVolumeApplied)
{
//Stop the fade
twoTracks[i].FadeVolume = towardsVolumeBgmVolumeApplied;
}
}
else
{
twoTracks[i].FadeVolume += Time.unscaledDeltaTime / fadeLength[i];
if (twoTracks[i].FadeVolume >= towardsVolumeBgmVolumeApplied)
{
//Stop the fade
twoTracks[i].FadeVolume = towardsVolumeBgmVolumeApplied;
}
}
}
//Stop check
if (willStop[i] && twoTracks[i].FadeVolume == 0)
{
willStop[i] = false;
willPause[i] = false;
twoTracks[i].Stop();
UnloadTrack(i);
}
//Pause check
if (willPause[i] && twoTracks[i].FadeVolume == 0)
{
willStop[i] = false;
willPause[i] = false;
twoTracks[i].Pause();
//don't unload!
}
}
}
private void UnloadTrack(int trackNumber)
{
//Have to check if other track is using the music or not?
//If playing the same song again,
//the loading of the next song might come earlier, then got immediately unloaded by this.
//Also check for when using different IntroloopAudio with the same source file.
//In this case .Music will be not equal, but actually the audioClip inside is the same song.
//Note that load/unloading has no effect on "Streaming" audio type.
var musicEqualCurrent = twoTracks[trackNumber].Music == twoTracks[(trackNumber + 1) % 2].Music;
var clipEqualCurrent = twoTracks[trackNumber].Music != null &&
twoTracks[(trackNumber + 1) % 2].Music != null &&
twoTracks[trackNumber].Music.AudioClip ==
twoTracks[(trackNumber + 1) % 2].Music.AudioClip;
//As = AudioSource
var isSameSongAsCurrent = musicEqualCurrent || clipEqualCurrent;
var musicEqualNext = twoTracks[trackNumber].Music == twoTracks[(trackNumber + 1) % 2].MusicAboutToPlay;
var clipEqualNext = twoTracks[trackNumber].Music != null &&
twoTracks[(trackNumber + 1) % 2].MusicAboutToPlay != null &&
twoTracks[trackNumber].Music.AudioClip ==
twoTracks[(trackNumber + 1) % 2].MusicAboutToPlay.AudioClip;
var isSameSongAsAboutToPlay = musicEqualNext || clipEqualNext;
var usingAndPlaying = twoTracks[(trackNumber + 1) % 2].IsPlaying && isSameSongAsCurrent;
if (!usingAndPlaying && !isSameSongAsAboutToPlay)
{
//If not, it is now safe to unload it
//Debug.Log("Unloading");
twoTracks[trackNumber].Unload();
}
}
internal void ApplyVolumeSettingToAllTracks()
{
twoTracks[0].ApplyVolume();
twoTracks[1].ApplyVolume();
}
/// <summary>
/// Play <see cref="IntroloopAudio"/> asset currently assigned to <see cref="DefaultIntroloopAudio"/>.
/// </summary>
/// <remarks>
/// <para>
/// It applies <see cref="IntroloopAudio.Volume"/> and <see cref="IntroloopAudio.Pitch"/>
/// to the underlying <see cref="AudioSource"/>.
/// </para>
/// <para>
/// If an another <see cref="IntroloopAudio"/> is playing on this player,
/// it could cross-fade between the two if <paramref name="fadeLengthSeconds"/> is provided.
/// The faded out audio will be unloaded automatically once the fade is finished.
/// </para>
/// </remarks>
/// <param name="fadeLengthSeconds">
/// Fade in/out length to use in seconds.
/// <list type="bullet">
/// <item>
/// <description>If 0, it uses a small pop removal fade time.</description>
/// </item>
/// <item>
/// <description>If negative, it is immediate.</description>
/// </item>
/// </list>
/// The audio will be unloaded only after it had fade out completely.
/// </param>
/// <param name="startTime">
/// <para>
/// Specify starting point in time instead of starting from the beginning.
/// </para>
/// <para>
/// The time you specify here will be converted to "play head time", Introloop will make the play head
/// at the point in time as if you had played for this amount of time before starting.
/// </para>
/// <para>
/// Since <see cref="IntroloopAudio"/> conceptually has infinite length, any number that is over looping boundary
/// will be wrapped over to the intro boundary in the calculation. (Except that if the audio is non-looping)
/// </para>
/// <para>
/// The time specified here is <b>not</b> taking <see cref="IntroloopAudio.Pitch"/> into account.
/// It's an elapsed time as if <see cref="IntroloopAudio.Pitch"/> is 1.
/// </para>
/// </param>
/// <exception cref="ArgumentNullException">
/// Thrown when <see cref="DefaultIntroloopAudio"/> was not assigned.
/// </exception>
public void Play(float fadeLengthSeconds = 0, float startTime = 0)
{
if (defaultIntroloopAudio == null)
{
throw new ArgumentNullException(nameof(defaultIntroloopAudio),
"Default Introloop Audio was not assigned, but you called " +
"Play overload without IntroloopAudio argument.");
}
Play(defaultIntroloopAudio, fadeLengthSeconds, startTime);
}
/// <summary>
/// Play any <see cref="IntroloopAudio"/> asset with the argument <paramref name="introloopAudio"/>,
/// regardless of <see cref="IntroloopAudio"/> asset assigned in <see cref="DefaultIntroloopAudio"/>.
/// </summary>
/// <remarks>
/// <para>
/// It applies <see cref="IntroloopAudio.Volume"/> and <see cref="IntroloopAudio.Pitch"/>
/// to the underlying <see cref="AudioSource"/>.
/// </para>
/// <para>
/// If an another <see cref="IntroloopAudio"/> is playing on this player,
/// it could cross-fade between the two if <paramref name="fadeLengthSeconds"/> is provided.
/// The faded out audio will be unloaded automatically once the fade is finished.
/// </para>
/// </remarks>
/// <param name="introloopAudio">
/// A reference to <see cref="IntroloopAudio"/> asset file to play.
/// </param>
/// <param name="fadeLengthSeconds">
/// Fade in/out length to use in seconds.
/// <list type="bullet">
/// <item>
/// <description>If 0, it uses a small pop removal fade time.</description>
/// </item>
/// <item>
/// <description>If negative, it is immediate.</description>
/// </item>
/// </list>
/// The audio will be unloaded only after it had fade out completely.
/// </param>
/// <param name="startTime">
/// <para>
/// Specify starting point in time instead of starting from the beginning.
/// </para>
/// <para>
/// The time you specify here will be converted to "play head time", Introloop will make the play head
/// at the point in time as if you had played for this amount of time before starting.
/// </para>
/// <para>
/// Since <see cref="IntroloopAudio"/> conceptually has infinite length, any number that is over looping boundary
/// will be wrapped over to the intro boundary in the calculation. (Except that if the audio is non-looping)
/// </para>
/// <para>
/// The time specified here is <b>not</b> taking <see cref="IntroloopAudio.Pitch"/> into account.
/// It's an elapsed time as if <see cref="IntroloopAudio.Pitch"/> is 1.
/// </para>
/// </param>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="introloopAudio"/> is `null`.</exception>
public void Play(IntroloopAudio introloopAudio, float fadeLengthSeconds = 0, float startTime = 0)
{
if (introloopAudio == null)
{
throw new ArgumentNullException(nameof(introloopAudio), "Played IntroloopAudio is null");
}
//Auto cross fade old ones. If no fade length specified, a very very small fade will be used to avoid pops/clicks.
Stop(fadeLengthSeconds);
var next = (currentTrack + 1) % 2;
twoTracks[next].Play(introloopAudio, fadeLengthSeconds == 0 ? false : true, startTime);
towardsVolume[next] = 1;
fadeLength[next] = TranslateFadeLength(fadeLengthSeconds);
currentTrack = next;
previousPlay = introloopAudio;
}
/// <summary>
/// Similar to <see cref="Play(IntroloopAudio, float, float)"/> overload, but has only a single
/// argument so it is able to receive calls from <see cref="UnityEvent"/>.
/// </summary>
public void Play(IntroloopAudio introloopAudio)
{
Play(introloopAudio, 0);
}
/// <summary>
/// Similar to <see cref="Play(IntroloopAudio, float, float)"/> overload, but has no
/// optional arguments so it is able to receive calls from <see cref="UnityEvent"/>.
/// </summary>
public void Play()
{
Play(0);
}
/// <summary>
/// Move the play head of the currently playing audio to anywhere in terms of elapsed time.
/// <list type="bullet">
/// <item>
/// <description>
/// If it is currently playing, you can instantly move the play head position to anywhere else.
/// </description>
/// </item>
/// <item>
/// <description>
/// If it is not playing, no effect. (This includes while in paused state, you cannot seek in paused state.)
/// </description>
/// </item>
/// </list>
/// </summary>
/// <remarks>
/// <para>
/// An internal implementation is not actually a seek, but a completely new
/// <see cref="Play(IntroloopAudio, float, float)"/> with the previous <see cref="IntroloopAudio"/>.
/// </para>
/// <para>
/// This is why you cannot seek while in pause, as it actually does a new play for you.
/// It is handy because it doesn't require you to remember and specify that audio again.
/// </para>
/// </remarks>
/// <param name="elapsedTime">
/// <para>
/// Introloop will make the play head at the point in time as if you had played for this amount
/// of time before starting.
/// </para>
/// <para>
/// The time you specify here will be converted to "play head time", Introloop will make the play head
/// at the point in time as if you had played for this amount of time before starting.
/// </para>
/// <para>
/// Since <see cref="IntroloopAudio"/> conceptually has infinite length, any number that is over looping boundary
/// will be wrapped over to the intro boundary in the calculation. (Except that if the audio is non-looping)
/// The time specified here is <b>not</b> taking <see cref="IntroloopAudio.Pitch"/> into account.
/// It's an elapsed time as if <see cref="IntroloopAudio.Pitch"/> is 1.
/// </para>
/// </param>
public void Seek(float elapsedTime)
{
if (!twoTracks[currentTrack].IsPlaying)
{
return;
}
twoTracks[currentTrack].Play(previousPlay, false, elapsedTime);
towardsVolume[currentTrack] = 1;
fadeLength[currentTrack] = 0;
}
/// <summary>
/// Stop the currently playing <see cref="IntroloopAudio"/> immediately and unload it from memory.
/// </summary>
public void Stop()
{
willStop[currentTrack] = false;
willPause[currentTrack] = false;
fadeLength[currentTrack] = 0;
twoTracks[currentTrack].FadeVolume = 0;
twoTracks[currentTrack].Stop();
UnloadTrack(currentTrack);
}
/// <summary>
/// Fading out to stop the currently playing <see cref="IntroloopAudio"/>, and unload it from memory
/// once it is completely faded out.
/// </summary>
/// <param name="fadeLengthSeconds">
/// Fade out length to use in seconds.
/// <list type="bullet">
/// <item>
/// <description>0 is a special value that will still apply small pop removal fade time.</description>
/// </item>
/// <item>
/// <description>If negative, this method works like <see cref="Stop()"/> overload.</description>
/// </item>
/// </list>
/// </param>
public void Stop(float fadeLengthSeconds)
{
if (fadeLengthSeconds < 0)
{
Stop();
}
else
{
willStop[currentTrack] = true;
willPause[currentTrack] = false;
fadeLength[currentTrack] = TranslateFadeLength(fadeLengthSeconds);
towardsVolume[currentTrack] = 0;
}
}
/// <summary>
/// Pause the currently playing <see cref="IntroloopAudio"/> immediately without unloading.
/// Call <see cref="Resume(float)"/> to continue playing.
/// </summary>
public void Pause()
{
if (twoTracks[currentTrack].IsPausable())
{
willStop[currentTrack] = false;
willPause[currentTrack] = false;
fadeLength[currentTrack] = 0;
twoTracks[currentTrack].FadeVolume = 0;
twoTracks[currentTrack].Pause();
}
}
/// <summary>
/// Fading out to pause the currently playing <see cref="IntroloopAudio"/> without unloading.
/// Call <see cref="Resume(float)"/> to continue playing.
/// </summary>
/// <param name="fadeLengthSeconds">
/// Fade out length to use in seconds.
/// <list type="bullet">
/// <item>
/// <description>0 is a special value that will still apply small pop removal fade time.</description>
/// </item>
/// <item>
/// <description>If negative, this method works like <see cref="Pause()"/> overload.</description>
/// </item>
/// </list>
/// </param>
public void Pause(float fadeLengthSeconds)
{
if (twoTracks[currentTrack].IsPausable())
{
if (fadeLengthSeconds < 0)
{
Pause();
}
else
{
willPause[currentTrack] = true;
willStop[currentTrack] = false;
fadeLength[currentTrack] = TranslateFadeLength(fadeLengthSeconds);
;
towardsVolume[currentTrack] = 0;
}
}
}
/// <summary>
/// Resume playing of previously paused (<see cref="Pause(float)"/>) <see cref="IntroloopAudio"/>.
/// If currently not pausing, it does nothing.
/// </summary>
/// <remarks>
/// Note that if it is currently "fading to pause", the state is not considered paused
/// yet so you can't resume in that time.
/// </remarks>
/// <param name="fadeLengthSeconds">
/// Fade out length to use in seconds.
/// <list type="bullet">
/// <item>
/// <description>If 0, it uses a small pop removal fade time.</description>
/// </item>
/// <item>
/// <description>If negative, it resumes immediately.</description>
/// </item>
/// </list>
/// </param>
public void Resume(float fadeLengthSeconds = 0)
{
if (twoTracks[currentTrack].Resume())
{
//Resume success
willStop[currentTrack] = false;
willPause[currentTrack] = false;
towardsVolume[currentTrack] = 1;
fadeLength[currentTrack] = TranslateFadeLength(fadeLengthSeconds);
}
}
/// <summary>
/// Zero length is a special value that equals pop removal small fade time.
/// Negative length is a special value that equals (real) 0.
/// </summary>
private static float TranslateFadeLength(float fadeLength)
{
return fadeLength > 0 ? fadeLength : fadeLength < 0 ? 0 : popRemovalFadeTime;
}
/// <summary>
/// An experimental feature in the case that you really want the audio to start
/// in an instant you call <see cref="Play(IntroloopAudio, float, float)"/>. You must use the same
/// <see cref="IntroloopAudio"/> that you preload in the next play.
/// </summary>
/// <remarks>
/// <para>
/// By normally using <see cref="Play(IntroloopAudio, float, float)"/> and <see cref="Stop(float)"/>
/// it loads the audio the moment you called <see cref="Play(IntroloopAudio, float, float)"/>.
/// Introloop waits for an audio to load before playing with a coroutine.
/// </para>
/// <para>
/// (Only if you have <see cref="AudioClip.loadInBackground"/> in the import settings.
/// Otherwise, <see cref="Play(IntroloopAudio, float, float)"/> will be a blocking call.)
/// </para>
/// <para>
/// Introloop can't guarantee that the playback will be instant,
/// but your game can continue while it is loading. By using this method before actually calling
/// <see cref="Play(IntroloopAudio, float, float)"/> it will instead be instant.
/// </para>
/// <para>
/// This function is special even songs with <see cref="AudioClip.loadInBackground"/>
/// can be loaded in a blocking fashion. (You can put <see cref="Play(IntroloopAudio, float, float)"/> immediately
/// in the next line expecting a fully loaded audio.)
/// </para>
/// <para>
/// However be aware that memory is managed less efficiently in the following case :
/// Normally Introloop immediately unloads the previous track to minimize memory.
/// But if you use <see cref="Preload(IntroloopAudio)"/> then did not call
/// <see cref="Play(IntroloopAudio, float, float)"/> with the same <see cref="IntroloopAudio"/> afterwards,
/// the loaded memory will be unmanaged.
/// </para>
/// <para>
/// (Just like if you tick <see cref="AudioClip.preloadAudioData"/> on your clip and have them
/// in a hierarchy somewhere, then did not use it.)
/// </para>
/// <para>
/// Does not work with <see cref="AudioClipLoadType.Streaming"/> audio loading type.
/// </para>
/// </remarks>
public void Preload(IntroloopAudio introloopAudio)
{
introloopAudio.Preload();
}
/// <summary>
/// This interpretation of a play time could decrease when it goes over
/// looping boundary back to intro boundary. Conceptually Introloop audio has infinite length,
/// so this time is a bit different from normal sense.
/// </summary>
/// <remarks>
/// <para>
/// Think as it as not "elapsed time" but rather the position of the actual playhead,
/// expressed in time as if the pitch is 1.
/// </para>
/// <para>
/// For example with pitch enabled, the play head will move slowly,
/// and so the time returned from this method respect that slower play head.
/// </para>
/// <para>
/// It is usable with <see cref="Play(IntroloopAudio, float, float)"/> as a start time
/// to "restore" the play from remembered time. With only 1 <see cref="IntroloopPlayer"/> you can stop and
/// unload previous song then continue later after reloading it.
/// </para>
/// <para>
/// Common use case includes battle music which resumes the field music afterwards.
/// If the battle is memory consuming unloading the field music could help.
/// </para>
/// </remarks>
public float GetPlayheadTime()
{
return twoTracks[currentTrack].PlayheadPositionSeconds;
}
/// <summary>
/// Assign a different audio mixer group to all underlying <see cref="AudioSource"/>.
/// </summary>
public void SetMixerGroup(AudioMixerGroup audioMixerGroup)
{
foreach (var aSource in InternalAudioSources)
{
aSource.outputAudioMixerGroup = audioMixerGroup;
}
}
/// <summary>
/// Call this before the first use of <see cref="Instance"/> to have the singleton instance
/// copy <see cref="AudioSource"/> fields from <paramref name="templateSource"/>.
/// </summary>
/// <remarks>
/// <para>
/// Singleton instance is convenient but you cannot pre-connect <see cref="TemplateSource"/> like
/// a regular instance because it does not exist until runtime.
/// </para>
/// <para>
/// If you had already used the singleton instance before calling this, you can still call
/// <see cref="ApplyAudioSource"/> on the singleton instance to apply different
/// settings of <see cref="AudioSource"/>.
/// </para>
/// </remarks>
public static void SetSingletonInstanceTemplateSource(AudioSource templateSource)
{
singletonInstanceTemplateSource = templateSource;
}
/// <summary>
/// <para>
/// Copy fields from <paramref name="applyFrom"/> to all 4 underlying <see cref="AudioSource"/>.
/// Make it as if they had <paramref name="applyFrom"/> as a <see cref="TemplateSource"/> from
/// the beginning. (Or you can think this method as a way to late-assign a <see cref="TemplateSource"/>.)
/// </para>
/// </summary>
/// <remarks>
/// The <see cref="AudioSource"/> does not need to be <see cref="MonoBehaviour.enabled"/> since it just
/// need to read the fields out for copy. Also it does not need to be anywhere on the scene,
/// it can come from a prefab with <see cref="AudioSource"/> in your project.
/// </remarks>
public void ApplyAudioSource(AudioSource applyFrom)
{
foreach (var aSource in InternalAudioSources)
{
CopyAudioSourceFields(aSource, applyFrom);
}
}
private static void CopyAudioSourceFields(AudioSource copyTo, AudioSource copyFrom)
{
// Pitch is NOT inherited, that could destroy scheduling!
// Pitch can only be specified via the IntroloopAudio asset file.
copyTo.outputAudioMixerGroup = copyFrom.outputAudioMixerGroup;
copyTo.SetCustomCurve(AudioSourceCurveType.CustomRolloff,
copyFrom.GetCustomCurve(AudioSourceCurveType.CustomRolloff));
copyTo.SetCustomCurve(AudioSourceCurveType.ReverbZoneMix,
copyFrom.GetCustomCurve(AudioSourceCurveType.ReverbZoneMix));
copyTo.SetCustomCurve(AudioSourceCurveType.SpatialBlend,
copyFrom.GetCustomCurve(AudioSourceCurveType.SpatialBlend));
copyTo.SetCustomCurve(AudioSourceCurveType.Spread,
copyFrom.GetCustomCurve(AudioSourceCurveType.Spread));
// Spatial blend, reverb zone mix, and spread must NOT be copied
// since it will overwrite the curve copy above.
copyTo.ignoreListenerVolume = copyFrom.ignoreListenerVolume;
copyTo.ignoreListenerPause = copyFrom.ignoreListenerPause;
copyTo.velocityUpdateMode = copyFrom.velocityUpdateMode;
copyTo.panStereo = copyFrom.panStereo;
// applyTarget.spatialBlend = applyFrom.spatialBlend;
copyTo.spatialize = copyFrom.spatialize;
copyTo.spatializePostEffects = copyFrom.spatializePostEffects;
// applyTarget.reverbZoneMix = applyFrom.reverbZoneMix;
copyTo.bypassEffects = copyFrom.bypassEffects;
copyTo.bypassListenerEffects = copyFrom.bypassListenerEffects;
copyTo.bypassReverbZones = copyFrom.bypassReverbZones;
copyTo.dopplerLevel = copyFrom.dopplerLevel;
// applyTarget.spread = applyFrom.spread;
copyTo.priority = copyFrom.priority;
copyTo.mute = copyFrom.mute;
copyTo.minDistance = copyFrom.minDistance;
copyTo.maxDistance = copyFrom.maxDistance;
}
/// <summary>
/// Each player contains 4 <see cref="AudioSource"/>, this method
/// returns the current information of the first pair for debugging purpose.
/// </summary>
public string[] GetDebugStringsTrack1()
{
return twoTracks[0].DebugInformation;
}
/// <summary>
/// Each player contains 4 <see cref="AudioSource"/>, this method
/// returns the current information of the second pair for debugging purpose.
/// </summary>
public string[] GetDebugStringsTrack2()
{
return twoTracks[1].DebugInformation;
}
}
}