Files
2026-07-13 13:13:17 +08:00

206 lines
7.1 KiB
C#

using T3.Core.Animation;
using T3.Core.Utils;
namespace Lib.io.audio._;
[Guid("1fa651c8-ab73-4ca0-9506-84602bbf2fcb")]
/// <summary>
/// This is an older implementation. A slighted updated
/// algorithm can be found in <see cref="Interaction.Timing.BpmDetection"/>
/// </summary>
internal sealed class DetectBeatOffset : Instance<DetectBeatOffset>
{
[Output(Guid = "c53fb442-4dd7-473c-8a1f-1adaabe3bcf7")]
public readonly Slot<List<float>> Measurements = new();
[Output(Guid = "68794F45-9537-4E88-91D7-6C4E93205BBC")]
public readonly Slot<float> Peak = new();
[Output(Guid = "819469C8-7178-4712-9EB5-7BC7AEFA32D8")]
public readonly Slot<float> Confidence = new();
public DetectBeatOffset()
{
Measurements.UpdateAction += Update;
Peak.UpdateAction += Update;
}
private class TimeEntry
{
public double Time;
public float[] FftValues;
}
private double _lastEvalTime;
//private List<float[]> _slicesForSmoothing = new List<float[]>();
private void Update(EvaluationContext context)
{
var inputFft = FftInput.GetValue(context);
if (inputFft == null || inputFft.Count == 0)
{
return;
}
var timeSinceLastEval = Playback.RunTimeInSecs - _lastEvalTime;
if (timeSinceLastEval < 0.01)
return;
_lastEvalTime = Playback.RunTimeInSecs;
const float assumedFrameRate = 60f;
//var timeSinceLastUpdate = Playback.LastFrameDuration;
//var fillCount = (int)Math.Round(timeSinceLastUpdate / (1 / assumedFrameRate) - 0.5f) + 1;
var historyDurationInSecs = 20f;
var maxFftHistoryLength = (int)(assumedFrameRate * historyDurationInSecs);
var currentTime = context.Playback.TimeInSecs;
var maxValueCount = 8;
if (inputFft.Count < maxValueCount)
{
return;
}
// Down sample
var values = new float[maxValueCount];
var delta = (inputFft.Count / (float)maxValueCount);
var sourceIndex = 0f;
for (var valueIndex = 0; valueIndex < maxValueCount && sourceIndex <= inputFft.Count; valueIndex++)
{
values[valueIndex] = inputFft[(int)sourceIndex];
sourceIndex += delta;
}
// const int smoothWindow = 3;
// _slicesForSmoothing.Insert(0, values);
// if(_slicesForSmoothing.Count >= smoothWindow)
// _slicesForSmoothing.RemoveRange(smoothWindow , _slicesForSmoothing.Count - smoothWindow );
// Smooth
// var smoothedValues = new float[maxValueCount];
// for (var valueIndex = 0; valueIndex < maxValueCount; valueIndex++)
// {
// var s = 0f;
// for (var smoothIndex = 0; smoothIndex < _slicesForSmoothing.Count; smoothIndex++)
// {
// s += _slicesForSmoothing[smoothIndex][valueIndex];
// }
// smoothedValues[valueIndex] = s / _slicesForSmoothing.Count;
// }
_fftHistory.Insert(0, new TimeEntry()
{
Time= currentTime,
FftValues= values,
});
// Clamp history to length of buffer
if(_fftHistory.Count >= maxFftHistoryLength)
_fftHistory.RemoveRange(maxFftHistoryLength , _fftHistory.Count - maxFftHistoryLength);
//Log.Debug($"Slice count {sliceIndexForBar} {durationOfMeasure:0.00} {context.Playback.Bpm}", this);
var shiftCount = 25;
// Swipe scan
_results.Clear();
var min = float.PositiveInfinity;
var max = float.NegativeInfinity;
var maxIndex = 0;
var maxValue = float.NegativeInfinity;
var durationOfMeasure = context.Playback.SecondsFromBars(1);
var sliceIndexForBar = (int)(durationOfMeasure * assumedFrameRate + 0.5f);
var sliceLength = sliceIndexForBar * 4;
//Log.Debug($"her length historyLenght: {maxFftHistoryLength} offset:{sliceIndexForBar} bpm:{context.Playback.Bpm:0.0} bar: {durationOfMeasure:0.00}s lenght:{sliceLength}",this);
for (int index = 0 ; index < shiftCount; index++)
//for (var shiftIndex = - shiftCount / 2; shiftIndex < shiftCount /2; shiftIndex++)
{
var shiftIndex = index - shiftCount / 2;
var energyDifference = GetEnergyDifference(shiftIndex + sliceIndexForBar, sliceLength);
min = MathF.Min(energyDifference, min);
max = MathF.Max(energyDifference, max);
_results.Add(energyDifference );
if (energyDifference > maxValue)
{
//maxIndex = index;
maxValue = energyDifference;
}
}
// Remap values
var totalSum = 0f;
var sum = 0f;
for (var index = 0; index < _results.Count; index++)
{
var remappedValue = MathF.Pow(MathUtils.RemapAndClamp(_results[index], min, max, 0,1),2);
totalSum += remappedValue * index;
sum += remappedValue;
_results[index] = remappedValue;
}
var meanIndex = totalSum / sum - (shiftCount /2f);
Confidence.Value = (10 / (MathF.Abs(meanIndex - maxIndex) + 0.001f)).Clamp(0, 1);
Peak.Value = meanIndex;
Measurements.Value = _results;
Peak.DirtyFlag.Clear();
Measurements.DirtyFlag.Clear();
}
private List<float> _results = new();
private float GetEnergyDifference(int offset, int sliceLength)
{
float energyDifference = 0;
for (var sliceIndex = 0; sliceIndex < sliceLength; sliceIndex++)
{
if (sliceIndex >= _fftHistory.Count)
{
continue;
}
int shiftedIndex = sliceIndex + offset;
if (shiftedIndex < 0 || shiftedIndex >= _fftHistory.Count)
{
continue;
}
var valuesNew = _fftHistory[sliceIndex].FftValues;
var valuesOld = _fftHistory[shiftedIndex].FftValues;
var sliceWidth = Math.Min(valuesNew.Length, valuesOld.Length);
if (sliceWidth == 0)
{
continue;
}
for (var valueIndex = 0; valueIndex < sliceWidth; valueIndex++)
{
var diff = MathF.Abs(valuesNew[valueIndex] - valuesOld[valueIndex]);
//var energy = 1 / (diff + 0.1f);
var energy = MathF.Pow(diff, 2);
energyDifference += energy;
//energyDifference = MathF.Pow() * 100,2f) / sliceWidth;
}
}
return 1/ energyDifference;
}
private readonly List<TimeEntry> _fftHistory = new();
[Input(Guid = "09628f30-9148-477e-a375-a7e661952ee5")]
public readonly InputSlot<List<float>> FftInput = new(new List<float>(20));
}