#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; } }