229 lines
7.7 KiB
C#
229 lines
7.7 KiB
C#
#nullable enable
|
|
using T3.Core.Animation;
|
|
using T3.Core.Audio;
|
|
|
|
namespace T3.Editor.Gui.Interaction.Timing;
|
|
|
|
/// <summary>
|
|
/// A helper to provide continuous a BeatTime for live situations.
|
|
/// The timing can be driven by BPM detection or beat tapping.
|
|
///
|
|
/// Its result is used by <see cref="BeatTimingPlayback"/> to drive playback.
|
|
/// </summary>
|
|
internal static class BeatTiming
|
|
{
|
|
internal static double BeatTime { get; private set; }
|
|
internal static float Bpm => (float)(60f / _beatDuration);
|
|
|
|
internal static void SetBpmRate(float bpm)
|
|
{
|
|
_beatDuration = 60f / bpm;
|
|
_tapTimes.Clear();
|
|
_resynced = false;
|
|
}
|
|
|
|
internal static void TriggerSyncTap() => _tapTriggeredLastFrame = true;
|
|
internal static void TriggerResyncMeasure() => _syncMeasureTriggeredLastFrame = true;
|
|
|
|
internal static void Update()
|
|
{
|
|
var playback = Playback.Current;
|
|
var playbackSettings = playback.Settings;
|
|
if (playbackSettings == null)
|
|
{
|
|
Log.Warning("Can't BeatTiming.Update() without playback settings");
|
|
return;
|
|
}
|
|
|
|
var runTime = Playback.RunTimeInSecs;
|
|
var distanceToMeasure = 1 - (float)Math.Abs((BeatTime % 4) / 4 - 0.5) * 2;
|
|
BeatTimingDetails.DistanceToMeasure = distanceToMeasure;
|
|
|
|
var distanceToBar = 1 - (float)Math.Abs((BeatTime % 1) / 1 - 0.5) * 2;
|
|
BeatTimingDetails.DistanceToBeat = distanceToBar;
|
|
|
|
var tappedBeatSync = _tapTriggeredLastFrame;
|
|
_tapTriggeredLastFrame = false;
|
|
|
|
var tappedMeasureSync = _syncMeasureTriggeredLastFrame;
|
|
_syncMeasureTriggeredLastFrame = false;
|
|
if (tappedMeasureSync)
|
|
{
|
|
BeatSynchronizer.Resync(Bpm);
|
|
_resynced = true;
|
|
}
|
|
|
|
// Fix BPM if completely out of control
|
|
if (double.IsNaN(Bpm) || Bpm < 20 || Bpm > 600)
|
|
{
|
|
Log.Warning($"BPM {Bpm:0.0} out of range. Reverting to 120");
|
|
_beatDuration = 0.5; // 120bpm
|
|
_phaseDelta = 0;
|
|
_syncMeasureOffset = 0;
|
|
_tapTimes.Clear();
|
|
_resyncTimes.Clear();
|
|
}
|
|
|
|
ProcessBeatTaps();
|
|
ProcessMeasureSyncTaps();
|
|
|
|
AdvanceBeatTime();
|
|
UpdateDebugData();
|
|
return;
|
|
|
|
//--------------------------------------------------------
|
|
|
|
|
|
void ProcessBeatTaps()
|
|
{
|
|
if (!tappedBeatSync)
|
|
return;
|
|
|
|
_resynced = false;
|
|
|
|
BeatTimingDetails.LastTapDistanceToBeat = (float)GetDeltaToSync(runTime);
|
|
|
|
AppendRunTimeToQueue(_tapTimes, MaxTapsCount, 2 * MeasureDuration);
|
|
if (_tapTimes.Count < 4)
|
|
{
|
|
_phaseDelta = 0;
|
|
return;
|
|
}
|
|
|
|
var lastTapTime = _tapTimes[^1];
|
|
if (runTime - lastTapTime > 4)
|
|
{
|
|
_phaseDelta = 0;
|
|
_tapTimes.Clear();
|
|
return;
|
|
}
|
|
|
|
var durationSum = 0.0;
|
|
var phaseOffsetSum = 0.0;
|
|
var lastT = 0.0;
|
|
|
|
for (var tapIndex = 0; tapIndex < _tapTimes.Count; tapIndex++)
|
|
{
|
|
var time = _tapTimes[tapIndex];
|
|
var dt = time - lastT;
|
|
|
|
lastT = time;
|
|
if (tapIndex == 0)
|
|
continue;
|
|
|
|
phaseOffsetSum += GetDeltaToSync(time);
|
|
durationSum += dt;
|
|
}
|
|
|
|
var stepCount = _tapTimes.Count - 1;
|
|
_beatDuration = durationSum / stepCount;
|
|
_phaseDelta = phaseOffsetSum / stepCount;
|
|
}
|
|
|
|
void ProcessMeasureSyncTaps()
|
|
{
|
|
if (!tappedMeasureSync)
|
|
return;
|
|
|
|
if (distanceToMeasure > 0.1)
|
|
{
|
|
_resyncTimes.Clear();
|
|
_resyncTimes.Add(runTime);
|
|
Log.Debug($"Skipping Sync as tap {distanceToBar:0.00} < {Threshold:0.00}");
|
|
}
|
|
else
|
|
{
|
|
AppendRunTimeToQueue(_resyncTimes, 8, 8.5 * MeasureDuration);
|
|
if (_resyncTimes.Count > 1)
|
|
{
|
|
var totalMeasureDuration = runTime - _resyncTimes[0];
|
|
var totalMeasureCount = Math.Round(totalMeasureDuration / MeasureDuration);
|
|
|
|
var originalBpm = Bpm;
|
|
_beatDuration = totalMeasureDuration / totalMeasureCount / BeatsPerMeasure;
|
|
Log.Debug($"Refining BPM rate {originalBpm:0.00} -> {Bpm:0.00}. ( over {totalMeasureDuration:0.00s}s)");
|
|
}
|
|
}
|
|
|
|
_syncMeasureOffset = -(runTime - _measureStartTime) / MeasureDuration;
|
|
}
|
|
|
|
void AdvanceBeatTime()
|
|
{
|
|
var timeInMeasure = runTime - _measureStartTime;
|
|
if (timeInMeasure > MeasureDuration)
|
|
{
|
|
_measureCount++;
|
|
_measureStartTime += MeasureDuration;
|
|
}
|
|
|
|
if (playback.Settings.Playback.EnableAudioBeatLocking && _resynced)
|
|
{
|
|
BeatTime = _barTimeAverage.UpdateAndCompute(BeatSynchronizer.BarProgress)
|
|
+ playback.BarsFromSeconds(playbackSettings.Playback.BeatLockAudioOffsetSec);
|
|
|
|
_beatDuration = (float)(60f / BeatSynchronizer.CurrentBpm);
|
|
}
|
|
else
|
|
{
|
|
var tInMeasure = (runTime - _measureStartTime) / MeasureDuration;
|
|
BeatTime = (_measureCount + tInMeasure + _syncMeasureOffset) * BeatsPerBar;
|
|
}
|
|
}
|
|
|
|
|
|
void UpdateDebugData()
|
|
{
|
|
BeatTimingDetails.WasResyncTriggered = tappedMeasureSync ? 1f : 0f;
|
|
BeatTimingDetails.WasTapTriggered = tappedBeatSync ? 1f : 0f;
|
|
|
|
BeatTimingDetails.Bpm = (float)Bpm;
|
|
BeatTimingDetails.SyncMeasureOffset = (float)_syncMeasureOffset;
|
|
BeatTimingDetails.BeatTime = (float)BeatTime;
|
|
BeatTimingDetails.LastPhaseOffset = (float)_phaseDelta;
|
|
BeatTimingDetails.MeasureDuration = (float)MeasureDuration;
|
|
}
|
|
|
|
void AppendRunTimeToQueue(IList<double> list, int maxLength, double maxDuration)
|
|
{
|
|
while (list.Count > maxLength || list.Count > 1 && runTime - list[0] > maxDuration)
|
|
{
|
|
list.RemoveAt(0);
|
|
}
|
|
|
|
list.Add(runTime);
|
|
}
|
|
}
|
|
|
|
private static double GetDeltaToSync(double time)
|
|
{
|
|
var timeInMeasure = time - _measureStartTime;
|
|
var beatCount = Math.Round(Math.Abs(timeInMeasure) / _beatDuration);
|
|
return timeInMeasure - _beatDuration * beatCount;
|
|
}
|
|
|
|
private static bool _resynced;
|
|
|
|
private static double MeasureDuration => _beatDuration * BeatsPerMeasure;
|
|
private const int MaxTapsCount = 16;
|
|
private static readonly List<double> _tapTimes = new(MaxTapsCount + 1);
|
|
private static readonly List<double> _resyncTimes = new(MaxTapsCount + 1);
|
|
|
|
private static int BeatsPerMeasure => BeatsPerBar * BarsPerMeasure;
|
|
private static int BarsPerMeasure { get; set; } = 4;
|
|
private static int BeatsPerBar { get; set; } = 4;
|
|
|
|
private static double _beatDuration = 1;
|
|
|
|
private static double _phaseDelta;
|
|
private static double _measureStartTime;
|
|
private static double _measureCount;
|
|
private static double _syncMeasureOffset;
|
|
|
|
private static bool _syncMeasureTriggeredLastFrame;
|
|
private static bool _tapTriggeredLastFrame;
|
|
|
|
private const double Threshold = 0.3;
|
|
|
|
private static readonly SlidingAverage _barTimeAverage = new(10);
|
|
} |