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.
690 lines
28 KiB
690 lines
28 KiB
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
|
|
namespace E7.Introloop
|
|
{
|
|
/// <summary>
|
|
/// <para>
|
|
/// Each track is 2 <see cref="AudioSource"/> scheduling to stitch up audio, with accurate scheduling methods.
|
|
/// Those methods cannot do immediate play, but could do precise play if the start time is in the future.
|
|
/// </para>
|
|
/// <para>
|
|
/// In this class we give reasonable time before the actual audio seam.
|
|
/// Schedule cannot be canceled, but stay "dormant" while in pause and resume.
|
|
/// So after coming back we need to reschedule properly.
|
|
/// </para>
|
|
/// </summary>
|
|
internal class IntroloopTrack : MonoBehaviour
|
|
{
|
|
/// <summary>
|
|
/// <para>
|
|
/// Theoretically playing "instantly" is impossible, even though we want it to play "right now"
|
|
/// on calling <see cref="IntroloopPlayer.Play(E7.Introloop.IntroloopAudio,float,float)"/>
|
|
/// </para>
|
|
/// <para>
|
|
/// So this time is the near-instant future that we told the scheduling method to start.
|
|
/// With this, it could get better chance that the first loop is accurate at the expense of some waiting time.
|
|
/// </para>
|
|
/// <para>
|
|
/// (But Introloop was never meant to be a low latency solution,
|
|
/// for that please search for [Native Audio](https://exceed7.com/native-audio/).)
|
|
/// </para>
|
|
/// <para>
|
|
/// Previously, you could randomly get a bit off first loop depending on whether it could fulfill this
|
|
/// "right now" or not. You notice that usually all loops except the first one are accurate,
|
|
/// that's because it got time far into the future as a schedule, unlike the first play.
|
|
/// </para>
|
|
/// <para>
|
|
/// The default number `0.02f` was chosen to be a bit more that 16ms,
|
|
/// the time per 1 frame on 60 FPS.So that it could get through a busy frame.
|
|
/// </para>
|
|
/// </summary>
|
|
private const float smallPrepareTime = 0.02f;
|
|
|
|
private double dspPlusHalfAudio;
|
|
|
|
private float fadeVolume;
|
|
private double introFinishDspTime;
|
|
internal IntroloopSettings introloopSettings;
|
|
|
|
private bool isPausing;
|
|
private bool isPausingOnIntro;
|
|
|
|
// There is one case that isPlaying is not correct, if non-looping music ends,
|
|
// this isPlaying is not updated to "false".
|
|
// In other case, music can only be stopped by user's command so I can set it to false at that moment.
|
|
// This is intentional, since it is not required for anything else and I don't want to
|
|
// waste a Coroutine just to correctly update this.
|
|
|
|
//This is used by IntroloopPlayer to check about unloading.
|
|
|
|
private int nextSourceToPlay;
|
|
|
|
/// <summary>
|
|
/// The next moment that we need to switch up AudioSource.
|
|
/// We must schedule way earlier than this to make it in time.
|
|
/// </summary>
|
|
private double nextTransitionTime;
|
|
|
|
private double pauseDspTimestamp;
|
|
|
|
/// <summary>
|
|
/// When resume, this variable will add up how long we have paused the audio.
|
|
/// Used in determining total playtime.
|
|
/// </summary>
|
|
private double pauseDspTotal;
|
|
|
|
private double playDspTimestamp;
|
|
private int playingSourceBeforePause;
|
|
private double rememberHowFarFromNextTransitionTime;
|
|
private double[] sourceWillEndTime;
|
|
private double[] sourceWillPlayTime;
|
|
private float startPlayingUnscaledClipTime;
|
|
|
|
/* ================================================================================================================
|
|
Note : UNITY (somewhat hidden) AUDIO RULES (Note to self/ to those who that wanted to edit it)
|
|
(This is from experiments from Unity 3.5 era, I am not sure if now the mechanics changed or not)
|
|
|
|
1. When Stop(),Pause() the PLAY/STOP schedule that met while stopping will be remembered
|
|
but won't execute UNTIL you call Play().
|
|
2. AudioSettings.dspTime is time-critical. A couple lines of code and call dspTime again, it's different!
|
|
3. According to 1. if you run a new schedule while Stop(), Pause().. the remembered schedule will be.......
|
|
3.1 If STOP schedule is remembered, your newly scheduled PLAY schedule WILL NOT win.
|
|
After your playSchedule execute the stopSchedule will follows. (stopSchedule always comes last?)
|
|
4. AudioListener.pause = true IS THE ONLY WAY to stop AudioSettings.dspTime so no worries when pausing like this :D
|
|
5. [audioSource].isPlaying will becomes TRUE after called PlayScheduled() (even if the schedule is not met yet!)
|
|
6. [audioSource].Stop() does not always reset the [audioSource].time to 0, but instead reset to whatever the last
|
|
[audioSource].time that you set to. (Which is default to 0!) / .Pause() will freeze .time at that instance.
|
|
7. According to 5. and 1. , Stop() and Pause() will override .isPlaying to FALSE, regardless of schedule or remembered
|
|
schedule.
|
|
8. Calling PlaySchedule while pausing can start the AudioSource (not calling any Play()) BUT another PlaySchedule
|
|
that have been met while pausing will not be overridden by new PlaySchedule's time. You must use SetScheduleStartTime
|
|
to reschedule it, alongside with PlaySchedule to start the audio. Using Play() instead of PlaySchedule is untested.
|
|
|
|
Awesome blog : https://johnleonardfrench.com/articles/ultimate-guide-to-playscheduled-in-unity/
|
|
================================================================================================================ */
|
|
|
|
private AudioSource[] twoSources;
|
|
|
|
internal IntroloopAudio Music { get; private set; }
|
|
|
|
internal IntroloopAudio MusicAboutToPlay { get; private set; }
|
|
|
|
internal bool IsPlaying { get; private set; }
|
|
|
|
/// <summary>
|
|
/// No matter where you start playing, this is the cumulative play time.
|
|
/// It stops on pause and it has no maximum or wrap back.
|
|
/// </summary>
|
|
internal float PlayedTimeSecondsUnscaled
|
|
{
|
|
get
|
|
{
|
|
double currentDspPlayhead = 0;
|
|
if (!IsPlaying && !isPausing)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
if (isPausing || !IsPlaying)
|
|
{
|
|
currentDspPlayhead = pauseDspTimestamp;
|
|
}
|
|
else
|
|
{
|
|
currentDspPlayhead = AudioSettings.dspTime;
|
|
}
|
|
|
|
return (float) (currentDspPlayhead - playDspTimestamp - pauseDspTotal);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// "Play head" is affected by pitch, they move slower with lower pitch.
|
|
/// </summary>
|
|
internal float PlayheadPositionSeconds
|
|
{
|
|
get
|
|
{
|
|
if (Music == null)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
var playedTime = PlayedTimeSecondsUnscaled;
|
|
|
|
// Convert time since played into audio's time. This time we need to know where was that started? Before intro or after?
|
|
// Also it would have "went too fast" if the actual played time was pitched down, so we need to get it to unscaled time.
|
|
|
|
if (Music.nonLooping)
|
|
{
|
|
return Mathf.Min(startPlayingUnscaledClipTime + playedTime * Music.Pitch, Music.UnscaledClipLength);
|
|
}
|
|
|
|
if (Music.loopWholeAudio)
|
|
{
|
|
return (startPlayingUnscaledClipTime + playedTime * Music.Pitch) % Music.UnscaledClipLength;
|
|
}
|
|
|
|
var playedUnscaled = startPlayingUnscaledClipTime + playedTime * Music.Pitch;
|
|
var beforeIntro = playedUnscaled < Music.UnscaledIntroLength;
|
|
if (beforeIntro)
|
|
{
|
|
return playedUnscaled;
|
|
}
|
|
|
|
return Music.UnscaledIntroLength +
|
|
(playedUnscaled - Music.UnscaledIntroLength) % Music.UnscaledLoopingLength;
|
|
}
|
|
}
|
|
|
|
public string[] DebugInformation
|
|
{
|
|
get
|
|
{
|
|
return new[]
|
|
{
|
|
"Play head position :" + PlayheadPositionSeconds.ToString(".00"),
|
|
"Source 1 Will Play :" + sourceWillPlayTime[0].ToString(".00"),
|
|
"Source 1 Will End :" + sourceWillEndTime[0].ToString(".00"),
|
|
"Source 2 Will Play :" + sourceWillPlayTime[1].ToString(".00"),
|
|
"Source 2 Will End :" + sourceWillEndTime[1].ToString(".00"),
|
|
"Source 1 : " + (twoSources[0].isPlaying ? "PLAYING/SCHEDULED" : "STOPPED"),
|
|
"Source 2 : " + (twoSources[1].isPlaying ? "PLAYING/SCHEDULED" : "STOPPED"),
|
|
"Source 1 Time : " + twoSources[0].time.ToString(".00"),
|
|
"Source 2 Time : " + twoSources[1].time.ToString(".00"),
|
|
"Next transition time : " + nextTransitionTime.ToString(".00"),
|
|
"Dsp plus half audio : " + dspPlusHalfAudio.ToString(".00"),
|
|
"Is pausing : " + isPausing,
|
|
};
|
|
}
|
|
}
|
|
|
|
internal float FadeVolume
|
|
{
|
|
get => fadeVolume;
|
|
set
|
|
{
|
|
var clampedValue = Mathf.Clamp01(value);
|
|
fadeVolume = clampedValue;
|
|
ApplyVolume();
|
|
}
|
|
}
|
|
|
|
internal IEnumerable<AudioSource> AllAudioSources
|
|
{
|
|
get
|
|
{
|
|
if (twoSources == null)
|
|
{
|
|
throw new IntroloopException(
|
|
"Introloop is not yet initialized. Please avoid accessing internal AudioSource on Awake.");
|
|
}
|
|
|
|
return twoSources;
|
|
}
|
|
}
|
|
|
|
private void Awake()
|
|
{
|
|
twoSources = new AudioSource[2];
|
|
sourceWillPlayTime = new double[2];
|
|
sourceWillEndTime = new double[2];
|
|
|
|
var gameObTransform = gameObject.transform;
|
|
|
|
var sourceObject1 = new GameObject("AudioSource 1");
|
|
var as1 = sourceObject1.AddComponent<AudioSource>();
|
|
as1.playOnAwake = false;
|
|
as1.loop = false;
|
|
sourceObject1.transform.parent = gameObTransform;
|
|
|
|
twoSources[0] = as1;
|
|
|
|
var sourceObject2 = new GameObject("AudioSource 2");
|
|
var as2 = sourceObject2.AddComponent<AudioSource>();
|
|
as2.playOnAwake = false;
|
|
as2.loop = false;
|
|
sourceObject2.transform.parent = gameObTransform;
|
|
|
|
twoSources[1] = as2;
|
|
}
|
|
|
|
internal void Unload()
|
|
{
|
|
Music.Unload();
|
|
IntroloopLog($"Unloaded \"{Music.AudioClip.name}\" from memory.");
|
|
twoSources[0].clip = null;
|
|
twoSources[1].clip = null;
|
|
Music = null;
|
|
MusicAboutToPlay = null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Check if it is the time to schedule the next stitch already or not.
|
|
/// </summary>
|
|
internal void SchedulingUpdate()
|
|
{
|
|
if (IsPlaying)
|
|
{
|
|
if (!Music.nonLooping) //In the case of non-looping, no scheduling happen at all.
|
|
{
|
|
dspPlusHalfAudio = AudioSettings.dspTime + Music.LoopingLength / 2f;
|
|
if (dspPlusHalfAudio > nextTransitionTime)
|
|
{
|
|
//Schedule halfway of looping audio.
|
|
ScheduleNextLoop();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ScheduleNextLoop()
|
|
{
|
|
// Note : (nextSourceToPlay + 1) % 2 is not always the same as "currently playing source"
|
|
// even though we have 2 tracks in total, because this "nextSourceToPlay" updates when next loop is "scheduled".
|
|
|
|
SetScheduledEndTime((nextSourceToPlay + 1) % 2, nextTransitionTime);
|
|
PlayScheduled(nextSourceToPlay, nextTransitionTime);
|
|
twoSources[nextSourceToPlay].time = Music.UnscaledIntroLength;
|
|
|
|
nextTransitionTime = nextTransitionTime + (Music.loopWholeAudio ? Music.ClipLength : Music.LoopingLength);
|
|
|
|
nextSourceToPlay = (nextSourceToPlay + 1) % 2;
|
|
//Debug.Log("IntroloopTrack : Next loop scheduled.");
|
|
}
|
|
|
|
internal void Stop()
|
|
{
|
|
twoSources[0].Stop();
|
|
twoSources[1].Stop();
|
|
pauseDspTimestamp = AudioSettings.dspTime;
|
|
|
|
//This is so that the schedule won't cancel the stop by itself
|
|
IsPlaying = false;
|
|
isPausing = false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// It cannot pause if neither source is playing, since pausing need to determine which one is playing,
|
|
/// so we could resume scheduling the correct one (while keeping their assigned audio, be it intro part
|
|
/// or looping part.)
|
|
/// </summary>
|
|
internal bool IsPausable()
|
|
{
|
|
// REMARKS : This method does not work at OnApplicationPause, at that moment all AudioSource
|
|
// will report isPlaying as FALSE regardless of its real status. I think it is 2019.1+ only bug.
|
|
if (!IsPlaying || !twoSources[0].isPlaying && !twoSources[1].isPlaying)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
internal void Pause()
|
|
{
|
|
if (!IsPlaying)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Note: On 2019.1+, OnApplicationPause, both are FALSE at the moment of game minimizing,
|
|
// So pausing at that moment won't work, since we need it to find out which one is playing.
|
|
// There is a bug that Unity loses all end schedule scheduled on minimize, probably connected to this.
|
|
|
|
if (twoSources[0].isPlaying && twoSources[1].isPlaying)
|
|
{
|
|
// Hard case, which one is not actually playing but scheduled?
|
|
// (Scheduled audio reports isPlaying as true)
|
|
|
|
if (AudioSettings.dspTime < sourceWillPlayTime[0])
|
|
{
|
|
playingSourceBeforePause = 1;
|
|
}
|
|
else
|
|
{
|
|
playingSourceBeforePause = 0;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Easy case
|
|
if (twoSources[0].isPlaying)
|
|
{
|
|
playingSourceBeforePause = 0;
|
|
}
|
|
else
|
|
{
|
|
playingSourceBeforePause = 1;
|
|
}
|
|
}
|
|
|
|
twoSources[0].Pause();
|
|
twoSources[1].Pause();
|
|
|
|
var pausingDspTime = AudioSettings.dspTime;
|
|
rememberHowFarFromNextTransitionTime = nextTransitionTime - pausingDspTime;
|
|
pauseDspTimestamp = pausingDspTime;
|
|
|
|
if (!Music.nonLooping && !Music.loopWholeAudio) //If contain intro
|
|
{
|
|
if (pausingDspTime >= introFinishDspTime)
|
|
{
|
|
isPausingOnIntro = false;
|
|
}
|
|
else
|
|
{
|
|
isPausingOnIntro = true;
|
|
}
|
|
}
|
|
|
|
// So the schedule won't cancel the stop by itself
|
|
IsPlaying = false;
|
|
isPausing = true;
|
|
IntroloopLog("\"" + Music.AudioClip.name + "\" paused.");
|
|
}
|
|
|
|
internal bool Resume()
|
|
{
|
|
if (!isPausing)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var sourceToContinuePlaying = playingSourceBeforePause;
|
|
|
|
// Rescheduling!
|
|
var absoluteTimeNow = AudioSettings.dspTime;
|
|
|
|
pauseDspTotal += absoluteTimeNow - pauseDspTimestamp;
|
|
|
|
float remainingTime;
|
|
if (!Music.nonLooping && !Music.loopWholeAudio) //If contain intro
|
|
{
|
|
if (isPausingOnIntro)
|
|
{
|
|
remainingTime = Music.IntroLength - twoSources[sourceToContinuePlaying].time / Music.Pitch;
|
|
}
|
|
else
|
|
{
|
|
remainingTime = Music.IntroLength + Music.LoopingLength -
|
|
twoSources[sourceToContinuePlaying].time / Music.Pitch;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
remainingTime = Music.ClipLength - twoSources[sourceToContinuePlaying].time / Music.Pitch;
|
|
}
|
|
|
|
// For current track
|
|
SetScheduledEndTime(sourceToContinuePlaying, absoluteTimeNow + remainingTime); //Intro has no tail!
|
|
|
|
// Order does not matter but both must exist.
|
|
SetScheduledStartTime(sourceToContinuePlaying, absoluteTimeNow);
|
|
PlayScheduled(sourceToContinuePlaying, absoluteTimeNow);
|
|
|
|
if (!Music.nonLooping)
|
|
{
|
|
// For next track
|
|
SetScheduledStartTime((sourceToContinuePlaying + 1) % 2, absoluteTimeNow + remainingTime);
|
|
PlayScheduled((sourceToContinuePlaying + 1) % 2, absoluteTimeNow + remainingTime);
|
|
}
|
|
|
|
if (isPausingOnIntro)
|
|
{
|
|
// For the case of pausing on intro too long (so long that the previously scheduled intro has finished)
|
|
introFinishDspTime = absoluteTimeNow + remainingTime;
|
|
}
|
|
|
|
nextTransitionTime = AudioSettings.dspTime + rememberHowFarFromNextTransitionTime;
|
|
|
|
IsPlaying = true;
|
|
isPausing = false;
|
|
IntroloopLog("\"" + Music.AudioClip.name + "\" resumed.");
|
|
return true;
|
|
}
|
|
|
|
internal void Play(IntroloopAudio music, bool isFade, float startTime)
|
|
{
|
|
pauseDspTimestamp = 0;
|
|
pauseDspTotal = 0;
|
|
|
|
twoSources[0].pitch = music.Pitch;
|
|
twoSources[1].pitch = music.Pitch;
|
|
|
|
var loadState = music.AudioClip.loadState;
|
|
var musicName = music.AudioClip.name;
|
|
FadeVolume = isFade ? 0 : 1;
|
|
if (loadState != AudioDataLoadState.Loaded)
|
|
{
|
|
IntroloopLog("\"" + musicName + "\" not loaded yet. Loading into memory...");
|
|
StartCoroutine(LoadAndPlayRoutine(music, startTime));
|
|
}
|
|
else
|
|
{
|
|
IntroloopLog("\"" + musicName + "\" already loaded in memory.");
|
|
SetupPlayingSchedule(music, startTime);
|
|
}
|
|
}
|
|
|
|
private IEnumerator LoadAndPlayRoutine(IntroloopAudio music, float startTime)
|
|
{
|
|
var musicName = music.AudioClip.name;
|
|
var startLoadingTime = Time.unscaledTime;
|
|
music.AudioClip.LoadAudioData();
|
|
while (music.AudioClip.loadState != AudioDataLoadState.Loaded &&
|
|
music.AudioClip.loadState != AudioDataLoadState.Failed)
|
|
{
|
|
yield return null;
|
|
}
|
|
|
|
if (music.AudioClip.loadState == AudioDataLoadState.Loaded)
|
|
{
|
|
var endLoadingTime = Time.unscaledTime;
|
|
if (music.AudioClip.loadInBackground)
|
|
{
|
|
IntroloopLog(musicName + " loading successful. (Took " + (endLoadingTime - startLoadingTime) +
|
|
" seconds loading in background.)");
|
|
}
|
|
else
|
|
{
|
|
IntroloopLog(musicName + " loading successful.");
|
|
}
|
|
|
|
SetupPlayingSchedule(music, startTime);
|
|
}
|
|
else
|
|
{
|
|
IntroloopLogError(musicName + " loading failed!");
|
|
}
|
|
}
|
|
|
|
private void SetupPlayingSchedule(IntroloopAudio music, float startTime)
|
|
{
|
|
IntroloopLog("Playing \"" + music.AudioClip.name + "\"");
|
|
|
|
MusicAboutToPlay = music;
|
|
|
|
MusicAboutToPlay = null;
|
|
Music = music;
|
|
ApplyVolume();
|
|
nextSourceToPlay = 0;
|
|
isPausing = false;
|
|
|
|
twoSources[0].clip = music.AudioClip;
|
|
twoSources[1].clip = music.AudioClip;
|
|
|
|
// Essential to cancel the Pause
|
|
Stop();
|
|
|
|
// It is important to "anchor" this somewhere and not calling AudioSettings.dspTime again later.
|
|
// Because its value changes even in-between lines of code.
|
|
var dspTimeNow = AudioSettings.dspTime + smallPrepareTime;
|
|
|
|
if (music.nonLooping)
|
|
{
|
|
if (startTime < music.UnscaledClipLength)
|
|
{
|
|
twoSources[0].time = startTime;
|
|
twoSources[1].time = 0;
|
|
|
|
// Note : you cannot just pull the value back from `twoSources[0].time`. The setter is not instantaneous!
|
|
startPlayingUnscaledClipTime = startTime;
|
|
|
|
PlayScheduled(0, dspTimeNow);
|
|
}
|
|
else
|
|
{
|
|
// Do not **actually play** if specified time overshoot.
|
|
// It could produce this error :
|
|
// `./Modules/Audio/Public/sound/SoundChannel.cpp(341) : Error executing result (An invalid seek position was passed to this function. )`
|
|
// The status turned into "play", but it is as if it had already finished.
|
|
|
|
twoSources[0].time = 0;
|
|
twoSources[1].time = 0;
|
|
startPlayingUnscaledClipTime = music.UnscaledClipLength;
|
|
}
|
|
|
|
IntroloopLog("Type : Non-looping");
|
|
}
|
|
else if (music.loopWholeAudio)
|
|
{
|
|
// This is just a simple loop at the end, but achieved with 2 sources in Introloop style.
|
|
// Yes it is an overkill.. but to streamline the process.
|
|
// (Also when in some case Unity 1-source loop is not seamless, you could try this)
|
|
|
|
// PlayScheduled does not reset the playhead!
|
|
|
|
var loopedStartTime = startTime % music.UnscaledClipLength;
|
|
twoSources[0].time = loopedStartTime;
|
|
// Always wait at the beginning regardless of intro boundary set.
|
|
twoSources[1].time = 0;
|
|
|
|
// For end time we need to scale, but also minus out the start time from the clip time, that is scaled too.
|
|
var dspEndMusicTime = dspTimeNow + music.ClipLength - startTime / music.Pitch;
|
|
PlayScheduled(0, dspTimeNow);
|
|
SetScheduledEndTime(0, dspEndMusicTime);
|
|
introFinishDspTime = dspEndMusicTime;
|
|
|
|
PlayScheduled(1, dspEndMusicTime);
|
|
nextTransitionTime = dspEndMusicTime + music.ClipLength;
|
|
startPlayingUnscaledClipTime = loopedStartTime;
|
|
|
|
IntroloopLog("Type : Loop whole audio");
|
|
}
|
|
else
|
|
{
|
|
var beforeIntro = startTime < music.UnscaledIntroLength;
|
|
|
|
if (beforeIntro)
|
|
{
|
|
// This is affected by pitch, since it will be used on pause & stuff that happen after play.
|
|
// Dont forget to minus out the start time that take up the intro length.
|
|
var dspIntroSeamTime = dspTimeNow + music.IntroLength - startTime / music.Pitch;
|
|
|
|
// The start time is in "play head time", we make it into regular AudioClip time.
|
|
twoSources[0].time = startTime;
|
|
|
|
// The 2nd source will wait at the intro part so it will go looping.
|
|
twoSources[1].time = music.UnscaledIntroLength;
|
|
|
|
PlayScheduled(0, dspTimeNow);
|
|
SetScheduledEndTime(0, dspIntroSeamTime);
|
|
introFinishDspTime = dspIntroSeamTime;
|
|
|
|
PlayScheduled(1, dspIntroSeamTime);
|
|
nextTransitionTime = dspIntroSeamTime + music.LoopingLength;
|
|
startPlayingUnscaledClipTime = startTime;
|
|
}
|
|
else
|
|
{
|
|
// The start time is still the "elapsed looping time", we make it into
|
|
// regular AudioClip "play head" time. Make sure it never overshoot.
|
|
var introloopedStartTime = music.UnscaledIntroLength +
|
|
(startTime - music.UnscaledIntroLength) % music.UnscaledLoopingLength;
|
|
|
|
// This is for the AudioClip starting time where it has no idea about the pitch.
|
|
var unscaledTimeIntoTheLoop = introloopedStartTime - music.UnscaledIntroLength;
|
|
var scaledTimeIntoTheLoop = unscaledTimeIntoTheLoop / music.Pitch;
|
|
var timeRemainingInTheLoop = music.LoopingLength - scaledTimeIntoTheLoop;
|
|
var dspLoopEndTime = dspTimeNow + timeRemainingInTheLoop;
|
|
|
|
twoSources[0].time = music.UnscaledIntroLength + unscaledTimeIntoTheLoop;
|
|
// If start time is over intro, the 2nd source must be waiting at the end instead.
|
|
twoSources[1].time = music.UnscaledIntroLength + music.UnscaledLoopingLength;
|
|
|
|
// This breaks our "schedule automatically half way until the loop point" policy,
|
|
// since it is now possible that we start way after that. The solution is to schedule next loop
|
|
// immediately, and change the state so that it looks like we had played through the intro.
|
|
PlayScheduled(0, dspTimeNow);
|
|
|
|
nextTransitionTime = dspLoopEndTime;
|
|
|
|
// As if the stitch had occured once.
|
|
nextSourceToPlay = 1;
|
|
ScheduleNextLoop();
|
|
|
|
// Intro would have already finished in this case. Time go backwards.
|
|
// This variable is required to make pause work.
|
|
introFinishDspTime = dspTimeNow - scaledTimeIntoTheLoop;
|
|
|
|
startPlayingUnscaledClipTime = music.UnscaledIntroLength + unscaledTimeIntoTheLoop;
|
|
;
|
|
}
|
|
|
|
IntroloopLog("Type : Introloop");
|
|
}
|
|
|
|
playDspTimestamp = dspTimeNow;
|
|
pauseDspTimestamp = 0;
|
|
pauseDspTotal = 0;
|
|
IsPlaying = true;
|
|
}
|
|
|
|
internal void ApplyVolume()
|
|
{
|
|
if (Music != null)
|
|
{
|
|
twoSources[0].volume = FadeVolume * Music.Volume;
|
|
twoSources[1].volume = FadeVolume * Music.Volume;
|
|
}
|
|
}
|
|
|
|
private void PlayScheduled(int sourceNumber, double absoluteTime)
|
|
{
|
|
//Debug.Log("Source " + sourceNumber + " play at " + absoluteTime);
|
|
twoSources[sourceNumber].PlayScheduled(absoluteTime);
|
|
sourceWillPlayTime[sourceNumber] = absoluteTime;
|
|
}
|
|
|
|
private void SetScheduledEndTime(int sourceNumber, double absoluteTime)
|
|
{
|
|
twoSources[sourceNumber].SetScheduledEndTime(absoluteTime);
|
|
sourceWillEndTime[sourceNumber] = absoluteTime;
|
|
}
|
|
|
|
private void SetScheduledStartTime(int sourceNumber, double absoluteTime)
|
|
{
|
|
twoSources[sourceNumber].SetScheduledStartTime(absoluteTime);
|
|
sourceWillPlayTime[sourceNumber] = absoluteTime;
|
|
}
|
|
|
|
internal void IntroloopLog(string logMessage)
|
|
{
|
|
if (introloopSettings.LogInformation)
|
|
{
|
|
Debug.Log("[Introloop - " + gameObject.name + "] " + logMessage);
|
|
}
|
|
}
|
|
|
|
internal void IntroloopLogError(string logMessage)
|
|
{
|
|
if (introloopSettings.LogInformation)
|
|
{
|
|
Debug.Log("<color=red>[Introloop - " + gameObject.name + "]</color> " + logMessage);
|
|
}
|
|
}
|
|
}
|
|
}
|