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

193 lines
8.2 KiB
C#

#nullable enable
using T3.Core.Animation;
using T3.Core.Audio;
using T3.Core.Utils;
using T3.Editor.Gui.Interaction.Timing;
namespace T3.Editor.Gui.Windows.RenderExport;
internal static class RenderTiming
{
public struct Runtime
{
public double TimingOverhang; // leftover time due to ms granularity
public bool AudioRecording;
}
public static void ApplyTimeRange( RenderSettings renderSettings)
{
switch (renderSettings.TimeRange)
{
case RenderSettings.TimeRanges.Custom:
case RenderSettings.TimeRanges.Continuous:
// No end to derive — Continuous runs until the user stops it; Custom keeps the user's values.
break;
case RenderSettings.TimeRanges.Loop:
{
var playback = Playback.Current;
var startInSeconds = playback.SecondsFromBars(playback.LoopRange.Start);
var endInSeconds = playback.SecondsFromBars(playback.LoopRange.End);
renderSettings.StartInBars = (float)SecondsToReferenceTime(startInSeconds, renderSettings.TimeReference, renderSettings.FrameRate);
renderSettings.EndInBars = (float)SecondsToReferenceTime(endInSeconds, renderSettings.TimeReference, renderSettings.FrameRate);
break;
}
case RenderSettings.TimeRanges.Soundtrack:
{
if (PlaybackUtils.TryFindingSoundtrack(out var handle, out _))
{
var playback = Playback.Current;
var clip = handle.Clip;
renderSettings.StartInBars = (float)SecondsToReferenceTime(playback.SecondsFromBars(clip.TimeRange.Start), renderSettings.TimeReference, renderSettings.FrameRate);
if (clip.TimeRange.End > clip.TimeRange.Start)
renderSettings.EndInBars = (float)SecondsToReferenceTime(playback.SecondsFromBars(clip.TimeRange.End), renderSettings.TimeReference, renderSettings.FrameRate);
else
renderSettings.EndInBars = (float)SecondsToReferenceTime(clip.LengthInSeconds, renderSettings.TimeReference, renderSettings.FrameRate);
}
break;
}
default:
throw new ArgumentOutOfRangeException(nameof(renderSettings.TimeRange), renderSettings.TimeRange, null);
}
}
public static double ConvertReferenceTime(double time, RenderSettings.TimeReferences oldRef, RenderSettings.TimeReferences newRef, float fps)
{
if (oldRef == newRef)
return time;
var seconds = ReferenceTimeToSeconds(time, oldRef, fps);
return SecondsToReferenceTime(seconds, newRef, fps);
}
public static double ConvertFps(double time, double oldFps, double newFps)
{
if (oldFps == 0 || newFps == 0) return time;
return time / oldFps * newFps;
}
public static double ReferenceTimeToSeconds(double time, RenderSettings.TimeReferences references, float fps)
{
var playback = Playback.Current;
switch (references)
{
case RenderSettings.TimeReferences.Bars:
return playback.SecondsFromBars(time);
case RenderSettings.TimeReferences.Seconds:
return time;
case RenderSettings.TimeReferences.Frames:
return fps != 0 ? time / fps : time / 60.0;
default:
return time;
}
}
public static double SecondsToReferenceTime(double seconds, RenderSettings.TimeReferences references, float fps)
{
var playback = Playback.Current;
switch (references)
{
case RenderSettings.TimeReferences.Bars:
return playback.BarsFromSeconds(seconds);
case RenderSettings.TimeReferences.Seconds:
return seconds;
case RenderSettings.TimeReferences.Frames:
return fps != 0 ? seconds * fps : seconds * 60.0;
default:
return seconds;
}
}
public static int ComputeFrameCount(in RenderSettings s)
{
var start = ReferenceTimeToSeconds(s.StartInBars, s.TimeReference, s.FrameRate);
var end = ReferenceTimeToSeconds(s.EndInBars, s.TimeReference, s.FrameRate);
return (int)Math.Round((end - start) * s.FrameRate);
}
public static void SetPlaybackTimeForFrame(RenderProcess.ExportSession session)
{
// get playback settings
var composition = T3.Editor.UiModel.ProjectHandling.ProjectView.Focused?.CompositionInstance;
if (composition == null)
{
Log.Warning("Can't find focused composition instance.");
return;
}
PlaybackUtils.FindCompositionSettingsForInstance(composition, out var instanceWithSettings, out var projectSettings);
// change global settings before calculating times
Playback.Current.Bpm = projectSettings.Playback.Bpm;
Playback.Current.PlaybackSpeed = 0.0;
Playback.Current.Settings = projectSettings;
Playback.Current.FrameSpeedFactor = session.Settings.FrameRate / 60.0f;
// time range
var startSecs = ReferenceTimeToSeconds(session.Settings.StartInBars, session.Settings.TimeReference, session.Settings.FrameRate);
var oldSecs = Playback.Current.TimeInSecs;
if (session.Settings.TimeRange == RenderSettings.TimeRanges.Continuous)
{
// Open-ended: advance steadily from the start, there is no end to interpolate toward.
Playback.Current.TimeInSecs = startSecs + session.FrameIndex / session.Settings.FrameRate;
}
else
{
var endSecs = startSecs + Math.Max(session.FrameCount - 1, 0) / session.Settings.FrameRate;
var progress = session.FrameCount <= 1 ? 0.0 : session.FrameIndex / (double)(session.FrameCount - 1);
Playback.Current.TimeInSecs = MathUtils.Lerp(startSecs, endSecs, progress);
}
var adaptedDelta = Math.Max(Playback.Current.TimeInSecs - oldSecs + session.Runtime.TimingOverhang, 0.0);
// audio clip for preview
if (projectSettings.TryGetMainSoundtrack(instanceWithSettings, out var soundtrack))
AudioEngine.UseSoundtrackClip(soundtrack, Playback.Current.TimeInSecs);
if (!session.Runtime.AudioRecording)
{
session.Runtime.TimingOverhang = 0.0;
adaptedDelta = 1.0 / session.Settings.FrameRate;
Playback.Current.IsRenderingToFile = true;
Playback.Current.PlaybackSpeed = 1.0;
AudioRendering.PrepareRecording(Playback.Current, session.Settings.FrameRate);
var requestedEndSecs = ReferenceTimeToSeconds(session.Settings.EndInBars, session.Settings.TimeReference, session.Settings.FrameRate);
var actualEndSecs = startSecs + session.FrameCount / session.Settings.FrameRate;
Log.Gated.VideoRender($"Requested recording from {startSecs:0.0000} to {requestedEndSecs:0.0000} seconds");
Log.Gated.VideoRender($"Actually recording from {startSecs:0.0000} to {actualEndSecs:0.0000} seconds due to frame raster");
Log.Gated.VideoRender($"Using {Playback.Current.Bpm} bpm");
session.Runtime.AudioRecording = true;
}
// update audio engine (respect looping etc.)
Playback.Current.Update();
var bufferLengthMs = (int)Math.Floor(1000.0 * adaptedDelta);
session.Runtime.TimingOverhang = adaptedDelta - bufferLengthMs / 1000.0;
session.Runtime.TimingOverhang = Math.Max(session.Runtime.TimingOverhang, 0.0);
AudioEngine.CompleteFrame(Playback.Current, bufferLengthMs / 1000.0);
}
public static void ReleasePlaybackTime(ref RenderSettings s, ref Runtime rt)
{
AudioRendering.EndRecording(Playback.Current, s.FrameRate);
Playback.Current.TimeInSecs = ReferenceTimeToSeconds(s.EndInBars, s.TimeReference, s.FrameRate);
Playback.Current.IsRenderingToFile = false;
Playback.Current.PlaybackSpeed = 0.0;
Playback.Current.FrameSpeedFactor = 1.0; // could use actual display frame rate
Playback.Current.Update();
rt.AudioRecording = false;
rt.TimingOverhang = 0.0;
}
}