Timekeeping Improvements and Small Optimizations (#544)

* make BeatActions coroutines instead of componentrs

* pooled scheduled sounds

implement S' entity seek

* remove debug prints from last two changes

* implement absolute time tracking

implement DSP time resyncing

* optimize GameManager

* update TMPro

* update IDE packages

* fix dsp sync making the drift worse

* fix issue with the JSL dll

* relocate debug print

* make scheduled pitch setter functional

* any cpu
This commit is contained in:
minenice55
2023-09-11 18:28:04 -04:00
committed by GitHub
parent 0acaafbebd
commit 60d29f19c6
157 changed files with 49650 additions and 2194 deletions

View File

@ -1,16 +1,25 @@
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Starpelly;
using Jukebox;
using Jukebox.Legacy;
using HeavenStudio.Util;
namespace HeavenStudio
{
// [RequireComponent(typeof(AudioSource))]
public class Conductor : MonoBehaviour
{
public struct AddedPitchChange
{
public double time;
public float pitch;
}
public List<AddedPitchChange> addedPitchChanges = new List<AddedPitchChange>();
// Song beats per minute
// This is determined by the song you're trying to sync up to
public float songBpm;
@ -25,18 +34,19 @@ namespace HeavenStudio
// Current song position, in seconds
private double songPos; // for Conductor use only
public float songPosition => (float) songPos;
public float songPosition => (float)songPos;
public double songPositionAsDouble => songPos;
// Current song position, in beats
public double songPosBeat; // for Conductor use only
public float songPositionInBeats => (float) songPosBeat;
public float songPositionInBeats => (float)songPosBeat;
public double songPositionInBeatsAsDouble => songPosBeat;
// Current time of the song
private double time;
double dspTime, lastDspTime;
double absTime, lastAbsTime;
double dspTime;
double absTime, absTimeAdjust;
double dspMargin = 128 / 44100.0;
// the dspTime we started at
private double dspStart;
@ -45,6 +55,7 @@ namespace HeavenStudio
DateTime startTime;
//the beat we started at
private double startPos;
private double startBeat;
public double startBeatAsDouble => startBeat;
@ -59,7 +70,7 @@ namespace HeavenStudio
// Conductor is currently playing song
public bool isPlaying;
// Conductor is currently paused, but not fully stopped
public bool isPaused;
@ -79,16 +90,40 @@ namespace HeavenStudio
public void SetTimelinePitch(float pitch)
{
if (pitch != 0 && pitch * minigamePitch != SongPitch)
{
Debug.Log("added pitch change " + pitch * minigamePitch + " at" + absTime);
addedPitchChanges.Add(new AddedPitchChange { time = absTime, pitch = pitch * minigamePitch });
}
timelinePitch = pitch;
musicSource.pitch = SongPitch;
}
public void SetMinigamePitch(float pitch)
{
if (pitch != 0 && pitch * timelinePitch != SongPitch)
{
Debug.Log("added pitch change " + pitch * timelinePitch + " at" + absTime);
addedPitchChanges.Add(new AddedPitchChange { time = absTime, pitch = pitch * timelinePitch });
}
minigamePitch = pitch;
musicSource.pitch = SongPitch;
}
public void SetMinigamePitch(float pitch, double beat)
{
BeatAction.New( this,
new List<BeatAction.Action> {
new BeatAction.Action(beat, delegate {
SetMinigamePitch(pitch);
}),
}
);
}
void Awake()
{
instance = this;
@ -97,13 +132,16 @@ namespace HeavenStudio
void Start()
{
musicSource.priority = 0;
AudioConfiguration config = AudioSettings.GetConfiguration();
dspMargin = 2 * (config.dspBufferSize / (double)config.sampleRate);
addedPitchChanges.Clear();
}
public void SetBeat(double beat)
{
var chart = GameManager.instance.Beatmap;
double offset = chart.data.offset;
double startPos = GetSongPosFromBeat(beat);
startPos = GetSongPosFromBeat(beat);
double dspTime = AudioSettings.dspTime;
@ -113,19 +151,32 @@ namespace HeavenStudio
SeekMusicToTime(startPos);
songPosBeat = GetBeatFromSongPos(time);
GameManager.instance.SetCurrentEventToClosest(beat);
}
public void Play(double beat)
{
if (isPlaying) return;
if (!isPaused)
{
AudioConfiguration config = AudioSettings.GetConfiguration();
dspMargin = 2 * (config.dspBufferSize / (double)config.sampleRate);
addedPitchChanges.Clear();
addedPitchChanges.Add(new AddedPitchChange { time = 0, pitch = SongPitch });
}
var chart = GameManager.instance.Beatmap;
double offset = chart.data.offset;
double dspTime = AudioSettings.dspTime;
absTimeAdjust = 0;
dspStart = dspTime;
startTime = DateTime.Now;
GameManager.instance.SortEventsList();
double startPos = GetSongPosFromBeat(beat);
startPos = GetSongPosFromBeat(beat);
firstBeatOffset = offset;
time = startPos;
@ -149,12 +200,8 @@ namespace HeavenStudio
}
songPosBeat = GetBeatFromSongPos(time);
startTime = DateTime.Now;
lastAbsTime = 0;
lastDspTime = AudioSettings.dspTime;
dspStart = dspTime;
startBeat = songPosBeat;
isPlaying = true;
isPaused = false;
}
@ -170,10 +217,15 @@ namespace HeavenStudio
public void Stop(double time)
{
if (absTimeAdjust != 0)
{
Debug.Log($"Last playthrough had a dsp (audio) drift of {absTimeAdjust}.\nConsider increasing audio buffer size if audio distortion was present.");
}
this.time = time;
songPos = time;
songPosBeat = 0;
absTimeAdjust = 0;
isPlaying = false;
isPaused = false;
@ -181,13 +233,48 @@ namespace HeavenStudio
musicSource.Stop();
}
void SeekMusicToTime(double startPos)
/// <summary>
/// stops playback of the audio without stopping beatkeeping
/// </summary>
public void StopOnlyAudio()
{
musicSource.Stop();
}
/// <summary>
/// fades out the audio over a duration
/// </summary>
/// <param name="duration">duration of the fade</param>
public void FadeOutAudio(float duration)
{
StartCoroutine(FadeOutAudioCoroutine(duration));
}
IEnumerator FadeOutAudioCoroutine(float duration)
{
float startVolume = musicSource.volume;
float endVolume = 0f;
float startTime = Time.time;
float endTime = startTime + duration;
while (Time.time < endTime)
{
float t = (Time.time - startTime) / duration;
musicSource.volume = Mathf.Lerp(startVolume, endVolume, t);
yield return null;
}
musicSource.volume = endVolume;
StopOnlyAudio();
}
void SeekMusicToTime(double fStartPos)
{
double offset = GameManager.instance.Beatmap.data.offset;
if (musicSource.clip != null && startPos < musicSource.clip.length - offset)
if (musicSource.clip != null && fStartPos < musicSource.clip.length - offset)
{
// https://www.desmos.com/calculator/81ywfok6xk
double musicStartDelay = -offset - startPos;
double musicStartDelay = -offset - fStartPos;
if (musicStartDelay > 0)
{
musicSource.timeSamples = 0;
@ -195,24 +282,13 @@ namespace HeavenStudio
else
{
int freq = musicSource.clip.frequency;
int samples = (int)(freq * (startPos + offset));
int samples = (int)(freq * (fStartPos + offset));
musicSource.timeSamples = samples;
}
}
}
double deltaTimeReal { get {
double ret = absTime - lastAbsTime;
lastAbsTime = absTime;
return ret;
}}
double deltaTimeDsp { get {
double ret = dspTime - lastDspTime;
lastDspTime = dspTime;
return ret;
}}
public void Update()
{
if (isPlaying)
@ -229,24 +305,54 @@ namespace HeavenStudio
musicSource.UnPause();
musicScheduledPitch = SongPitch;
musicScheduledTime = (AudioSettings.dspTime + (-GameManager.instance.Beatmap.data.offset - songPositionAsDouble)/(double)SongPitch);
musicScheduledTime = (AudioSettings.dspTime + (-GameManager.instance.Beatmap.data.offset - songPositionAsDouble) / (double)SongPitch);
musicSource.SetScheduledStartTime(musicScheduledTime);
}
}
absTime = (DateTime.Now - startTime).TotalSeconds;
//dspTime to sync with audio thread in case of drift
dspTime = AudioSettings.dspTime - dspStart;
double dt = deltaTimeReal;
if (Math.Abs(absTime + absTimeAdjust - dspTime) > dspMargin)
{
int i = 0;
while (Math.Abs(absTime + absTimeAdjust - dspTime) > dspMargin)
{
i++;
absTimeAdjust = (dspTime - absTime + absTimeAdjust) * 0.5;
if (i > 8) break;
}
}
//todo: dspTime to sync with audio thread in case of drift
time = MapTimeToPitchChanges(absTime + absTimeAdjust);
time += dt * SongPitch;
songPos = time;
songPos = startPos + time;
songPosBeat = GetBeatFromSongPos(songPos);
}
}
double MapTimeToPitchChanges(double time)
{
double counter = 0;
double lastChangeTime = 0;
float pitch = addedPitchChanges[0].pitch;
foreach (var pch in addedPitchChanges)
{
double changeTime = pch.time;
if (changeTime > time)
{
break;
}
counter += (changeTime - lastChangeTime) * pitch;
lastChangeTime = changeTime;
pitch = pch.pitch;
}
counter += (time - lastChangeTime) * pitch;
return counter;
}
public void LateUpdate()
{
@ -265,7 +371,7 @@ namespace HeavenStudio
{
if (metronomeSound != null)
{
metronomeSound.Delete();
metronomeSound.Stop();
metronomeSound = null;
}
}
@ -352,53 +458,53 @@ namespace HeavenStudio
break;
}
counter += (t.beat - lastTempoChangeBeat) * 60/bpm;
counter += (t.beat - lastTempoChangeBeat) * 60 / bpm;
bpm = t["tempo"];
lastTempoChangeBeat = t.beat;
}
counter += (beat - lastTempoChangeBeat) * 60/bpm;
counter += (beat - lastTempoChangeBeat) * 60 / bpm;
return counter;
}
//thank you @wooningcharithri#7419 for the psuedo-code
public double BeatsToSecs(double beats, float bpm)
{
return beats / bpm * 60f;
}
public double SecsToBeats(double s, float bpm)
{
return s / 60f * bpm;
}
public double BeatsToSecs(double beats, float bpm)
{
return beats / bpm * 60f;
}
public double SecsToBeats(double s, float bpm)
{
return s / 60f * bpm;
}
public double GetBeatFromSongPos(double seconds)
{
double lastTempoChangeBeat = 0f;
double counterSeconds = 0;
float lastBpm = 120f;
foreach (RiqEntity t in GameManager.instance.Beatmap.TempoChanges)
{
double beatToNext = t.beat - lastTempoChangeBeat;
double secToNext = BeatsToSecs(beatToNext, lastBpm);
double nextSecs = counterSeconds + secToNext;
public double GetBeatFromSongPos(double seconds)
{
double lastTempoChangeBeat = 0f;
double counterSeconds = 0;
float lastBpm = 120f;
if (nextSecs >= seconds)
break;
lastTempoChangeBeat = t.beat;
lastBpm = t["tempo"];
counterSeconds = nextSecs;
}
return lastTempoChangeBeat + SecsToBeats(seconds - counterSeconds, lastBpm);
foreach (RiqEntity t in GameManager.instance.Beatmap.TempoChanges)
{
double beatToNext = t.beat - lastTempoChangeBeat;
double secToNext = BeatsToSecs(beatToNext, lastBpm);
double nextSecs = counterSeconds + secToNext;
if (nextSecs >= seconds)
break;
lastTempoChangeBeat = t.beat;
lastBpm = t["tempo"];
counterSeconds = nextSecs;
}
return lastTempoChangeBeat + SecsToBeats(seconds - counterSeconds, lastBpm);
}
//
// convert real seconds to beats
public double GetRestFromRealTime(double seconds)
{
return seconds/pitchedSecPerBeat;
return seconds / pitchedSecPerBeat;
}
public void SetBpm(float bpm)