From defff4f29c927dab2b5df3d26890407f621b02e6 Mon Sep 17 00:00:00 2001 From: N00MKRAD <61149547+n00mkrad@users.noreply.github.com> Date: Thu, 28 Nov 2024 16:08:04 +0100 Subject: [PATCH] Full VFR handling including FPS downsampling to CFR --- CodeLegacy/Data/Enums.cs | 2 + CodeLegacy/Data/Fraction.cs | 28 +++--- CodeLegacy/Data/InterpSettings.cs | 49 ++-------- CodeLegacy/Data/MediaFile.cs | 33 +------ CodeLegacy/Flowframes.csproj | 1 + CodeLegacy/Forms/BatchForm.cs | 3 - CodeLegacy/Forms/Main/Form1.cs | 23 ----- CodeLegacy/IO/IoUtils.cs | 4 +- CodeLegacy/Magick/Dedupe.cs | 18 ++-- CodeLegacy/Main/Export.cs | 37 +++----- CodeLegacy/Main/Interpolate.cs | 8 +- CodeLegacy/Main/InterpolateSteps.cs | 12 +-- CodeLegacy/Main/InterpolateUtils.cs | 20 ++-- CodeLegacy/Media/FfmpegAudioAndMetadata.cs | 3 +- CodeLegacy/Media/FfmpegEncode.cs | 3 +- CodeLegacy/Media/FfmpegExtract.cs | 13 +-- CodeLegacy/Media/FfmpegUtils.cs | 2 +- CodeLegacy/Media/TimestampUtils.cs | 104 +++++++++++++++++++++ CodeLegacy/Os/VapourSynthUtils.cs | 9 ++ CodeLegacy/Ui/ControlExtensions.cs | 5 + CodeLegacy/Ui/InterpolationProgress.cs | 2 +- 21 files changed, 187 insertions(+), 192 deletions(-) create mode 100644 CodeLegacy/Media/TimestampUtils.cs diff --git a/CodeLegacy/Data/Enums.cs b/CodeLegacy/Data/Enums.cs index a6f7a81b..cbca6a88 100644 --- a/CodeLegacy/Data/Enums.cs +++ b/CodeLegacy/Data/Enums.cs @@ -2,6 +2,8 @@ { public class Enums { + public enum Round { Near, Up, Down } + public class Output { public enum Format { Mp4, Mkv, Webm, Mov, Avi, Gif, Images, Realtime }; diff --git a/CodeLegacy/Data/Fraction.cs b/CodeLegacy/Data/Fraction.cs index 2e118093..9af1409b 100644 --- a/CodeLegacy/Data/Fraction.cs +++ b/CodeLegacy/Data/Fraction.cs @@ -2,12 +2,14 @@ namespace Flowframes.Data { - public struct Fraction + public class Fraction { - public long Numerator; - public long Denominator; + public long Numerator = 0; + public long Denominator = 1; public static Fraction Zero = new Fraction(0, 0); + public Fraction() { } + public Fraction(long numerator, long denominator) { this.Numerator = numerator; @@ -22,18 +24,6 @@ public Fraction(long numerator, long denominator) } } - public Fraction(long numerator, Fraction denominator) - { - //divide the numerator by the denominator fraction - this = new Fraction(numerator, 1) / denominator; - } - - public Fraction(Fraction numerator, long denominator) - { - //multiply the numerator fraction by 1 over the denominator - this = numerator * new Fraction(1, denominator); - } - public Fraction(Fraction fraction) { Numerator = fraction.Numerator; @@ -44,7 +34,9 @@ public Fraction(float value) { Numerator = (value * 10000f).RoundToInt(); Denominator = 10000; - this = GetReduced(); + var reducedFrac = GetReduced(); + Numerator = reducedFrac.Numerator; + Denominator = reducedFrac.Denominator; } public Fraction(string text) @@ -76,7 +68,9 @@ public Fraction(string text) else { // Use float constructor if not a whole number - this = new Fraction(numFloat); + var floatFrac = new Fraction(numFloat); + Numerator = floatFrac.Numerator; + Denominator = floatFrac.Denominator; } return; diff --git a/CodeLegacy/Data/InterpSettings.cs b/CodeLegacy/Data/InterpSettings.cs index ce1f890b..be9eb5b4 100644 --- a/CodeLegacy/Data/InterpSettings.cs +++ b/CodeLegacy/Data/InterpSettings.cs @@ -4,12 +4,9 @@ using Flowframes.Main; using Flowframes.MiscUtils; using System; -using System.Collections.Generic; using System.Drawing; using System.IO; using System.Linq; -using System.Threading.Tasks; -using System.Diagnostics; namespace Flowframes { @@ -19,10 +16,11 @@ public class InterpSettings public string outPath; public string FullOutPath { get; set; } = ""; public AiInfo ai; - public string inPixFmt = "yuv420p"; public Fraction inFps; public Fraction inFpsDetected; public Fraction outFps; + public Fraction outFpsResampled; + public bool FpsResampling => outFpsResampled != null && outFpsResampled.Float > 0.1f && outFpsResampled.Float < outFps.Float; public float outItsScale; public float interpFactor; public OutputSettings outSettings; @@ -86,56 +84,23 @@ public Size InterpResolution public InterpSettings() { } - public InterpSettings(string inPathArg, string outPathArg, AiInfo aiArg, Fraction inFpsDetectedArg, Fraction inFpsArg, float interpFactorArg, float itsScale, OutputSettings outSettingsArg, ModelCollection.ModelInfo modelArg) - { - inPath = inPathArg; - outPath = outPathArg; - ai = aiArg; - inFpsDetected = inFpsDetectedArg; - inFps = inFpsArg; - interpFactor = interpFactorArg; - outItsScale = itsScale; - outSettings = outSettingsArg; - model = modelArg; - - InitArgs(); - } - public void InitArgs () { outFps = inFps * (double)interpFactor; - + outFpsResampled = new Fraction(Config.Get(Config.Key.maxFps)); alpha = false; stepByStep = false; - framesExt = ""; interpExt = ""; - - try - { - tempFolder = InterpolateUtils.GetTempFolderLoc(inPath, outPath); - framesFolder = Path.Combine(tempFolder, Paths.framesDir); - interpFolder = Path.Combine(tempFolder, Paths.interpDir); - inputIsFrames = IoUtils.IsPathDirectory(inPath); - } - catch - { - Logger.Log("Tried to create InterpSettings struct without an inpath. Can't set tempFolder, framesFolder and interpFolder.", true); - tempFolder = ""; - framesFolder = ""; - interpFolder = ""; - inputIsFrames = false; - } - _inputResolution = new Size(0, 0); - + SetPaths(inPath); RefreshExtensions(ai: ai); } - public void UpdatePaths (string inPathArg, string outPathArg) + private void SetPaths (string inputPath) { - inPath = inPathArg; - outPath = outPathArg; + inPath = inputPath; + outPath = (Config.GetInt("outFolderLoc") == 0) ? inputPath.GetParentDir() : Config.Get("custOutDir").Trim(); tempFolder = InterpolateUtils.GetTempFolderLoc(inPath, outPath); framesFolder = Path.Combine(tempFolder, Paths.framesDir); interpFolder = Path.Combine(tempFolder, Paths.interpDir); diff --git a/CodeLegacy/Data/MediaFile.cs b/CodeLegacy/Data/MediaFile.cs index d9110286..103efa97 100644 --- a/CodeLegacy/Data/MediaFile.cs +++ b/CodeLegacy/Data/MediaFile.cs @@ -24,7 +24,7 @@ public class MediaFile public string Format; public string Title; public string Language; - public Fraction? InputRate = null; + public Fraction InputRate; public long DurationMs; public int StreamCount; public int TotalKbits; @@ -42,6 +42,8 @@ public class MediaFile public bool IsVfr = false; public List InputTimestamps = new List(); public List InputTimestampDurations = new List(); + public List OutputTimestamps = new List(); + public List OutputFrameIndexes = null; public int FileCount = 1; public int FrameCount { get { return VideoStreams.Count > 0 ? VideoStreams[0].FrameCount : 0; } } @@ -140,35 +142,6 @@ private async Task LoadFormatInfo(string path) TotalKbits = (await GetVideoInfo.GetFfprobeInfoAsync(path, GetVideoInfo.FfprobeMode.ShowFormat, "bit_rate")).GetInt() / 1000; } - public List GetResampledTimestamps(List timestamps, double factor) - { - int originalCount = timestamps.Count; - int newCount = (int)Math.Round(originalCount * factor); - List resampledTimestamps = new List(); - - for (int i = 0; i < newCount; i++) - { - double x = i / factor; - - if (x >= originalCount - 1) - { - resampledTimestamps.Add(timestamps[originalCount - 1]); - } - else - { - int index = (int)Math.Floor(x); - double fraction = x - index; - float startTime = timestamps[index]; - float endTime = timestamps[index + 1]; - - float interpolatedTime = (float)(startTime + (endTime - startTime) * fraction); - resampledTimestamps.Add(interpolatedTime); - } - } - - return resampledTimestamps; - } - public string GetName() { if (IsDirectory) diff --git a/CodeLegacy/Flowframes.csproj b/CodeLegacy/Flowframes.csproj index f33b6d5d..08b6874d 100644 --- a/CodeLegacy/Flowframes.csproj +++ b/CodeLegacy/Flowframes.csproj @@ -494,6 +494,7 @@ + diff --git a/CodeLegacy/Forms/BatchForm.cs b/CodeLegacy/Forms/BatchForm.cs index 811a1c86..c3c3e731 100644 --- a/CodeLegacy/Forms/BatchForm.cs +++ b/CodeLegacy/Forms/BatchForm.cs @@ -153,9 +153,6 @@ public async Task LoadDroppedPaths (string[] droppedPaths, bool start = false) Logger.Log($"BatchForm: Dropped path: '{path}'", true); InterpSettings current = Program.mainForm.GetCurrentSettings(); - string outDir = (Config.GetInt("outFolderLoc") == 0) ? path.GetParentDir() : Config.Get("custOutDir").Trim(); - current.UpdatePaths(path, outDir); - current.inFpsDetected = await IoUtils.GetFpsFolderOrVideo(path); current.inFps = current.inFpsDetected; diff --git a/CodeLegacy/Forms/Main/Form1.cs b/CodeLegacy/Forms/Main/Form1.cs index 17b0ffd1..671ff3b7 100644 --- a/CodeLegacy/Forms/Main/Form1.cs +++ b/CodeLegacy/Forms/Main/Form1.cs @@ -321,29 +321,6 @@ public InterpSettings GetCurrentSettings() return s; } - public InterpSettings UpdateCurrentSettings(InterpSettings settings) - { - SetTab(interpOptsTab.Name); - string inPath = inputTbox.Text.Trim(); - - if (settings.inPath != inPath) // If path changed, get new instance - { - Logger.Log($"settings.inPath ({settings.inPath}) mismatches GUI inPath ({settings.inPath} - Returning fresh instance", true); - return GetCurrentSettings(); - } - - settings.inPath = inPath; - settings.ai = GetAi(); - settings.inFpsDetected = currInFpsDetected; - settings.inFps = currInFps; - settings.interpFactor = interpFactorCombox.GetFloat(); - settings.outFps = settings.inFps * settings.interpFactor; - settings.outSettings = GetOutputSettings(); - settings.model = GetModel(GetAi()); - - return settings; - } - public void LoadBatchEntry(InterpSettings entry) { inputTbox.Text = entry.inPath; diff --git a/CodeLegacy/IO/IoUtils.cs b/CodeLegacy/IO/IoUtils.cs index 8c520e12..89e48ed8 100644 --- a/CodeLegacy/IO/IoUtils.cs +++ b/CodeLegacy/IO/IoUtils.cs @@ -597,9 +597,7 @@ public static bool RenameFile(string path, string newName, bool alsoRenameExtens public static async Task GetCurrentExportFilename(bool fpsLimit, bool isImgSeq = false, bool includeExt = true) { InterpSettings curr = Interpolate.currentSettings; - string max = Config.Get(Config.Key.maxFps); - Fraction maxFps = max.Contains("/") ? new Fraction(max) : new Fraction(max.GetFloat()); - float fps = fpsLimit ? maxFps.Float : curr.outFps.Float; + float fps = fpsLimit ? curr.outFpsResampled.Float : curr.outFps.Float; Size outRes = curr.OutputResolution; // TODO: Replace with EncodeResolution once implemented? string pattern = Config.Get(Config.Key.exportNamePattern); diff --git a/CodeLegacy/Magick/Dedupe.cs b/CodeLegacy/Magick/Dedupe.cs index 8802491d..0c0ec82e 100644 --- a/CodeLegacy/Magick/Dedupe.cs +++ b/CodeLegacy/Magick/Dedupe.cs @@ -264,27 +264,21 @@ public static async Task CreateDupesFile(string framesPath, string ext) public static async Task CreateFramesFileVideo(string videoPath, bool loop) { - if (!Directory.Exists(Interpolate.currentSettings.tempFolder)) - Directory.CreateDirectory(Interpolate.currentSettings.tempFolder); - + Directory.CreateDirectory(Interpolate.currentSettings.tempFolder); Process ffmpeg = OsUtils.NewProcess(true); string baseCmd = $"/C cd /D {Path.Combine(IO.Paths.GetPkgPath(), IO.Paths.audioVideoDir).Wrap()}"; string mpDec = FfmpegCommands.GetMpdecimate(wrap: false); // FfmpegCommands.GetMpdecimate((int)FfmpegCommands.MpDecSensitivity.Normal, false); ffmpeg.StartInfo.Arguments = $"{baseCmd} & ffmpeg -loglevel debug -y -i {videoPath.Wrap()} -fps_mode vfr -vf {mpDec} -f null NUL 2>&1 | findstr keep_count:"; - List ffmpegOutputLines = (await Task.Run(() => OsUtils.GetProcStdOut(ffmpeg, true))).SplitIntoLines().Where(l => l.IsNotEmpty()).ToList(); + var ffmpegOutputLines = (await Task.Run(() => OsUtils.GetProcStdOut(ffmpeg, true))).SplitIntoLines(); var frames = new Dictionary>(); - var frameNums = new List(); int lastKeepFrameNum = 0; - for (int frameIdx = 0; frameIdx < ffmpegOutputLines.Count; frameIdx++) + for (int frameIdx = 0; frameIdx < ffmpegOutputLines.Length; frameIdx++) { string line = ffmpegOutputLines[frameIdx]; - bool drop = frameIdx != 0 && line.Contains(" drop ") && !line.Contains(" keep "); - // Console.WriteLine($"[Frame {frameIdx.ToString().PadLeft(6, '0')}] {(drop ? "DROP" : "KEEP")}"); - // frameNums.Add(lastKeepFrameNum); - if (!drop) + if (line.Contains(" keep pts:")) { if (!frames.ContainsKey(frameIdx) || frames[frameIdx] == null) { @@ -293,7 +287,7 @@ public static async Task CreateFramesFileVideo(string videoPath, bool loop) lastKeepFrameNum = frameIdx; } - else + else if (line.Contains(" drop pts:")) { frames[lastKeepFrameNum].Add(frameIdx); } @@ -301,6 +295,8 @@ public static async Task CreateFramesFileVideo(string videoPath, bool loop) var inputFrames = new List(frames.Keys); + Logger.Log($"Dedupe: Kept {inputFrames.Count}/{ffmpegOutputLines.Length} frames", true); + if (loop) { inputFrames.Add(inputFrames.First()); diff --git a/CodeLegacy/Main/Export.cs b/CodeLegacy/Main/Export.cs index 212a83e0..cf7e784d 100644 --- a/CodeLegacy/Main/Export.cs +++ b/CodeLegacy/Main/Export.cs @@ -19,8 +19,7 @@ namespace Flowframes.Main { class Export { - private static string MaxFps => Config.Get(Config.Key.maxFps); - private static Fraction MaxFpsFrac => new Fraction(MaxFps); + private static Fraction MaxFpsFrac => I.currentSettings.outFpsResampled; public static async Task ExportFrames(string path, string outFolder, OutputSettings exportSettings, bool stepByStep) { @@ -67,7 +66,7 @@ public static async Task ExportFrames(string path, string outFolder, OutputSetti } catch (Exception e) { - Logger.Log("FramesToVideo Error: " + e.Message, false); + Logger.Log($"{nameof(ExportFrames)} Error: {e.Message}", false); UiUtils.ShowMessageBox("An error occured while trying to convert the interpolated frames to a video.\nCheck the log for details.", UiUtils.MessageType.Error); } } @@ -79,7 +78,7 @@ public static async Task GetPipedFfmpegCmd(bool ffplay = false) bool fpsLimit = MaxFpsFrac.Float > 0f && s.outFps.Float > MaxFpsFrac.Float; bool gifInput = I.currentMediaFile.Format.Upper() == "GIF"; // If input is GIF, we don't need to check the color space etc VidExtraData extraData = gifInput ? new VidExtraData() : await FfmpegCommands.GetVidExtraInfo(s.inPath); - string extraArgsIn = await FfmpegEncode.GetFfmpegExportArgsIn(s.outFps, s.outItsScale, extraData.Rotation); + string extraArgsIn = await FfmpegEncode.GetFfmpegExportArgsIn(s.FpsResampling ? s.outFpsResampled : s.outFps, s.outItsScale, extraData.Rotation); string extraArgsOut = await FfmpegEncode.GetFfmpegExportArgsOut(fpsLimit ? MaxFpsFrac : new Fraction(), extraData, s.outSettings); // For EXR, force bt709 input flags. Not sure if this really does anything, EXR @@ -121,8 +120,7 @@ static async Task ExportImageSequence(string framesPath, bool stepByStep) Enums.Encoding.Encoder desiredFormat = I.currentSettings.outSettings.Encoder; string availableFormat = Path.GetExtension(IoUtils.GetFilesSorted(framesPath, "*.*")[0]).Remove(".").Upper(); - Fraction maxFps = new Fraction(MaxFps); - bool fpsLimit = maxFps.Float > 0f && I.currentSettings.outFps.Float > maxFps.Float; + bool fpsLimit = MaxFpsFrac.Float > 0f && I.currentSettings.outFps.Float > MaxFpsFrac.Float; bool encodeFullFpsSeq = !(fpsLimit && Config.GetInt(Config.Key.maxFpsMode) == 0); string framesFile = Path.Combine(framesPath.GetParentDir(), Paths.GetFrameOrderFilename(I.currentSettings.interpFactor)); @@ -141,8 +139,8 @@ static async Task ExportImageSequence(string framesPath, bool stepByStep) if (fpsLimit) { string outputFolderPath = Path.Combine(I.currentSettings.outPath, await IoUtils.GetCurrentExportFilename(fpsLimit: true, isImgSeq: true)); - Logger.Log($"Exporting {desiredFormat.ToString().Upper()} frames to '{Path.GetFileName(outputFolderPath)}' (Resampled to {maxFps} FPS)..."); - await FfmpegEncode.FramesToFrames(framesFile, outputFolderPath, 1, I.currentSettings.outFps, maxFps, desiredFormat, OutputUtils.GetImgSeqQ(I.currentSettings.outSettings)); + Logger.Log($"Exporting {desiredFormat.ToString().Upper()} frames to '{Path.GetFileName(outputFolderPath)}' (Resampled to {MaxFpsFrac} FPS)..."); + await FfmpegEncode.FramesToFrames(framesFile, outputFolderPath, 1, I.currentSettings.outFps, MaxFpsFrac, desiredFormat, OutputUtils.GetImgSeqQ(I.currentSettings.outSettings)); } if (!stepByStep) @@ -389,24 +387,19 @@ public static void MuxTimestamps(string vidPath) { if (I.currentSettings.dedupe) { - Logger.Log($"{nameof(MuxTimestamps)}: Dedupe was used; won't mux timestamps for '{vidPath}'", hidden: true); + Logger.Log($"{nameof(MuxTimestamps)}: Dedupe was used; won't mux timestamps.", hidden: true); return; } - Logger.Log($"{nameof(MuxTimestamps)}: Muxing timestamps for '{vidPath}'", hidden: true); - float avgDuration = I.currentMediaFile.InputTimestampDurations.Average(); - I.currentMediaFile.InputTimestamps.Add(I.currentMediaFile.InputTimestamps.Last() + avgDuration); // Add extra frame using avg. duration, needed for duration matching or looping - - var resampledTs = I.currentMediaFile.GetResampledTimestamps(I.currentMediaFile.InputTimestamps, I.currentSettings.interpFactor); - var tsFileLines = new List() { "# timecode format v2" }; - - for (int i = 0; i < (resampledTs.Count - 1); i++) + if(I.currentMediaFile.IsVfr && I.currentMediaFile.OutputFrameIndexes != null && I.currentMediaFile.OutputFrameIndexes.Count > 0) { - tsFileLines.Add((resampledTs[i] * 1000f).ToString("0.000000")); + Logger.Log($"{nameof(MuxTimestamps)}: CFR conversion due to FPS limit was applied (picked {I.currentMediaFile.OutputFrameIndexes.Count} frames for {I.currentSettings.outFpsResampled} FPS); won't mux timestamps.", hidden: true); + return; } - string tsFile = Path.Combine(Paths.GetSessionDataPath(), "ts.out.txt"); - File.WriteAllLines(tsFile, tsFileLines); + Logger.Log($"{nameof(MuxTimestamps)}: Muxing timestamps for '{vidPath}'", hidden: true); + string tsFile = Path.Combine(Paths.GetSessionDataPath(), "ts.txt"); + TimestampUtils.WriteTsFile(I.currentMediaFile.OutputTimestamps, tsFile); string outPath = Path.ChangeExtension(vidPath, ".tmp.mkv"); string args = $"mkvmerge --output {outPath.Wrap()} --timestamps \"0:{tsFile}\" {vidPath.Wrap()}"; var outputMux = NUtilsTemp.OsUtils.RunCommand($"cd /D {Path.Combine(Paths.GetPkgPath(), Paths.audioVideoDir).Wrap()} && {args}"); @@ -414,14 +407,14 @@ public static void MuxTimestamps(string vidPath) // Check if file exists and is not too small (min. 80% of input file) if (File.Exists(outPath) && ((double)new FileInfo(outPath).Length / (double)new FileInfo(vidPath).Length) > 0.8d) { - Logger.Log($"{nameof(MuxTimestamps)}: Deleting '{vidPath}' and moving '{outPath}' to '{vidPath}'", hidden: true); + Logger.Log($"{nameof(MuxTimestamps)}: Deleting original '{vidPath}' and moving muxed '{outPath}' to '{vidPath}'", hidden: true); File.Delete(vidPath); File.Move(outPath, vidPath); } else { - Logger.Log(outputMux, hidden: true); Logger.Log($"{nameof(MuxTimestamps)}: Timestamp muxing failed, keeping original video file", hidden: true); + Logger.Log(outputMux, hidden: true); IoUtils.TryDeleteIfExists(outPath); } } diff --git a/CodeLegacy/Main/Interpolate.cs b/CodeLegacy/Main/Interpolate.cs index c18cf636..5b1dbc88 100644 --- a/CodeLegacy/Main/Interpolate.cs +++ b/CodeLegacy/Main/Interpolate.cs @@ -17,6 +17,7 @@ using System.Windows.Forms; using Padding = Flowframes.Data.Padding; using Utils = Flowframes.Main.InterpolateUtils; +using System.Drawing.Imaging; namespace Flowframes { @@ -27,7 +28,7 @@ public class Interpolate public static MediaFile currentMediaFile; public static bool canceled = false; public static float InterpProgressMultiplier = 1f; - static Stopwatch sw = new Stopwatch(); + private static Stopwatch sw = new Stopwatch(); public static async Task Start() { @@ -45,6 +46,11 @@ public static async Task Start() Program.mainForm.SetStatus("Starting..."); sw.Restart(); + if (currentMediaFile.IsVfr) + { + TimestampUtils.CalcTimestamps(currentMediaFile, currentSettings); + } + if (!AutoEncodeResume.resumeNextRun && !(currentSettings.ai.Piped && !currentSettings.inputIsFrames /* && Config.GetInt(Config.Key.dedupMode) == 0) */)) { await GetFrames(); diff --git a/CodeLegacy/Main/InterpolateSteps.cs b/CodeLegacy/Main/InterpolateSteps.cs index 72e2eb75..a903613d 100644 --- a/CodeLegacy/Main/InterpolateSteps.cs +++ b/CodeLegacy/Main/InterpolateSteps.cs @@ -20,17 +20,7 @@ public static async Task Run(string step) canceled = false; Program.mainForm.SetWorking(true); - if(currentSettings == null) - { - Logger.Log($"[SBS] Getting new current settings", true); - currentSettings = Program.mainForm.GetCurrentSettings(); - } - else - { - Logger.Log($"[SBS] Updating current settings", true); - currentSettings = Program.mainForm.UpdateCurrentSettings(currentSettings); - } - + currentSettings = Program.mainForm.GetCurrentSettings(); currentSettings.RefreshAlpha(); currentSettings.stepByStep = true; diff --git a/CodeLegacy/Main/InterpolateUtils.cs b/CodeLegacy/Main/InterpolateUtils.cs index 897868a5..2a87c70c 100644 --- a/CodeLegacy/Main/InterpolateUtils.cs +++ b/CodeLegacy/Main/InterpolateUtils.cs @@ -70,7 +70,7 @@ public static int GetProgressWaitTime(int numFrames) public static string GetTempFolderLoc(string inPath, string outPath) { - string basePath = Path.Combine(Environment.GetEnvironmentVariable("LOCALAPPDATA"), "Temp", "Flowframes"); + string basePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Temp", "Flowframes"); int tempFolderLoc = Config.GetInt(Config.Key.tempFolderLoc); switch (tempFolderLoc) @@ -96,7 +96,7 @@ public static string GetTempFolderLoc(string inPath, string outPath) break; } - string folderName = Path.GetFileNameWithoutExtension(inPath).StripBadChars().Remove(" ").Trunc(30, false) + ".tmp"; + string folderName = Path.GetFileNameWithoutExtension(inPath).StripBadChars().Remove(" ").Trunc(35, false) + "_tmp"; return Path.Combine(basePath, folderName); } @@ -131,23 +131,17 @@ public static bool InputIsValid(InterpSettings s) passes = false; } - string fpsLimitValue = Config.Get(Config.Key.maxFps); - float fpsLimit = (fpsLimitValue.Contains("/") ? new Fraction(fpsLimitValue).Float : fpsLimitValue.GetFloat()); - int maxFps = s.outSettings.Encoder.GetInfo().MaxFramerate; + float fpsLimit = s.outFpsResampled.Float; + int maxEncoderFps = s.outSettings.Encoder.GetInfo().MaxFramerate; - if (passes && s.outFps.Float < 1f || (s.outFps.Float > maxFps && !(fpsLimit > 0 && fpsLimit <= maxFps))) + if (passes && s.outFps.Float < 1f || (s.outFps.Float > maxEncoderFps && !(fpsLimit > 0 && fpsLimit <= maxEncoderFps))) { string imgSeqNote = isFile ? "" : "\n\nWhen using an image sequence as input, you always have to specify the frame rate manually."; - UiUtils.ShowMessageBox($"Invalid output frame rate ({s.outFps.Float}).\nMust be 1-{maxFps}. Either lower the interpolation factor or use the \"Maximum Output Frame Rate\" option.{imgSeqNote}"); + UiUtils.ShowMessageBox($"Invalid output frame rate ({s.outFps.Float}).\nMust be 1-{maxEncoderFps}. Either lower the interpolation factor or use the \"Maximum Output Frame Rate\" option.{imgSeqNote}"); passes = false; } - float fpsLimitFloat = fpsLimitValue.GetFloat(); - - if (fpsLimitFloat > 0 && fpsLimitFloat < s.outFps.Float) - Interpolate.InterpProgressMultiplier = s.outFps.Float / fpsLimitFloat; - else - Interpolate.InterpProgressMultiplier = 1f; + I.InterpProgressMultiplier = s.FpsResampling ? s.outFps.Float / fpsLimit : 1f; if (!passes) I.Cancel("Invalid settings detected.", true); diff --git a/CodeLegacy/Media/FfmpegAudioAndMetadata.cs b/CodeLegacy/Media/FfmpegAudioAndMetadata.cs index 4a13d41d..156dd845 100644 --- a/CodeLegacy/Media/FfmpegAudioAndMetadata.cs +++ b/CodeLegacy/Media/FfmpegAudioAndMetadata.cs @@ -6,6 +6,7 @@ using Utils = Flowframes.Media.FfmpegUtils; using I = Flowframes.Interpolate; using Flowframes.Main; +using System.Linq; namespace Flowframes.Media { @@ -60,7 +61,7 @@ public static async Task MergeStreamsFromInput (string inputVideo, string interp string metaArg = (isMkv && meta) ? "-map 1:t?" : ""; // https://reddit.com/r/ffmpeg/comments/fw4jnh/how_to_make_ffmpeg_keep_attached_images_in_mkv_as/ string shortestArg = shortest ? "-shortest" : ""; - if (I.currentMediaFile.IsVfr) + if (I.currentMediaFile.IsVfr && I.currentMediaFile.OutputTimestamps.Any()) { Export.MuxTimestamps(tempPath); } diff --git a/CodeLegacy/Media/FfmpegEncode.cs b/CodeLegacy/Media/FfmpegEncode.cs index a56c6a54..31ba3b96 100644 --- a/CodeLegacy/Media/FfmpegEncode.cs +++ b/CodeLegacy/Media/FfmpegEncode.cs @@ -70,12 +70,13 @@ public static async Task GetFfmpegExportArgsOut(Fraction resampleFps, Vi { if (Interpolate.currentMediaFile.IsVfr && !Interpolate.currentSettings.dedupe) { - Logger.Log($"Ignoring {resampleFps.Float} FPS limit as this is currently unsupported for variable framerate videos."); + // Logger.Log($"Ignoring {resampleFps.Float} FPS limit as this is currently unsupported for variable framerate videos."); } else { filters.Add($"fps={resampleFps}"); } + // filters.Add($"fps={resampleFps}"); } if (Config.GetBool(Config.Key.keepColorSpace) && extraData.HasAllColorValues()) diff --git a/CodeLegacy/Media/FfmpegExtract.cs b/CodeLegacy/Media/FfmpegExtract.cs index 0d81e2a2..5c520daa 100644 --- a/CodeLegacy/Media/FfmpegExtract.cs +++ b/CodeLegacy/Media/FfmpegExtract.cs @@ -163,7 +163,7 @@ await Task.Run(async () => } } - static bool AreImagesCompatible(string inpath, int maxHeight) + private static bool AreImagesCompatible(string inpath, int maxHeight) { NmkdStopwatch sw = new NmkdStopwatch(); string[] validExtensions = Filetypes.imagesInterpCompat; // = new string[] { ".jpg", ".jpeg", ".png" }; @@ -204,17 +204,6 @@ static bool AreImagesCompatible(string inpath, int maxHeight) return false; } - Logger.Log($"---> TODO: Modulo check for images? Or just check later and apply padding before interpolation if needed...", hidden: true); - - // int div = GetModulo(); - // bool allDivBy2 = randomSamples.All(i => (i.Width % div == 0) && (i.Height % div == 0)); - // - // if (!allDivBy2) - // { - // Logger.Log($"Sequence not compatible: Not all image dimensions are divisible by {div}.", true); - // return false; - // } - bool allSmallEnough = randomSamples.All(i => (i.Height <= maxHeight)); if (!allSmallEnough) diff --git a/CodeLegacy/Media/FfmpegUtils.cs b/CodeLegacy/Media/FfmpegUtils.cs index c1770ebd..43860b09 100644 --- a/CodeLegacy/Media/FfmpegUtils.cs +++ b/CodeLegacy/Media/FfmpegUtils.cs @@ -37,7 +37,7 @@ public static async Task GetStreamCount(string path) return output.SplitIntoLines().Where(x => x.MatchesWildcard("*Stream #0:*: *: *")).Count(); } - public static async Task> GetStreams(string path, bool progressBar, int streamCount, Fraction? defaultFps, bool countFrames, MediaFile mediaFile = null) + public static async Task> GetStreams(string path, bool progressBar, int streamCount, Fraction defaultFps, bool countFrames, MediaFile mediaFile = null) { List streamList = new List(); diff --git a/CodeLegacy/Media/TimestampUtils.cs b/CodeLegacy/Media/TimestampUtils.cs new file mode 100644 index 00000000..3cb110e6 --- /dev/null +++ b/CodeLegacy/Media/TimestampUtils.cs @@ -0,0 +1,104 @@ +using Flowframes.Data; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Flowframes.Media +{ + public class TimestampUtils + { + public static void CalcTimestamps(MediaFile media, InterpSettings settings) + { + float avgDuration = media.InputTimestampDurations.Average(); + media.InputTimestamps.Add(media.InputTimestamps.Last() + avgDuration); // Add extra frame using avg. duration, needed for duration matching or looping + media.OutputTimestamps = StretchTimestamps(media.InputTimestamps, settings.interpFactor); + + if (settings.FpsResampling) + { + List timestampsWithInputIdx = ConvertToConstantFrameRate(media.OutputTimestamps, settings.outFpsResampled.Float); + media.OutputTimestamps = timestampsWithInputIdx.Select(x => x[0]).ToList(); + media.OutputFrameIndexes = timestampsWithInputIdx.Select(x => (int)x[1]).ToList(); + } + } + + public static string WriteTsFile(List timestamps, string path) + { + var lines = new List() { "# timecode format v2" }; + + foreach (var ts in timestamps) + { + lines.Add((ts * 1000f).ToString("0.000000")); + } + + File.WriteAllLines(path, lines); + return path; + } + + public static List StretchTimestamps(List timestamps, double factor) + { + int originalCount = timestamps.Count; + int newCount = (int)Math.Round(originalCount * factor); + List resampledTimestamps = new List(); + + for (int i = 0; i < newCount; i++) + { + double x = i / factor; + + if (x >= originalCount - 1) + { + resampledTimestamps.Add(timestamps[originalCount - 1]); + } + else + { + int index = (int)Math.Floor(x); + double fraction = x - index; + float startTime = timestamps[index]; + float endTime = timestamps[index + 1]; + + float interpolatedTime = (float)(startTime + (endTime - startTime) * fraction); + resampledTimestamps.Add(interpolatedTime); + } + } + + return resampledTimestamps; + } + + public static List ConvertToConstantFrameRate(List inputTimestamps, float targetFrameRate, Enums.Round roundMethod = Enums.Round.Near) + { + List outputTimestamps = new List(); // Resulting list of timestamps + float targetFrameInterval = 1.0f / targetFrameRate; // Interval for the target frame rate + float currentTargetTime = 0.0f; // Start time for the target frame rate + int index = 0; // Index for iterating through the input timestamps + + Console.WriteLine("--> Converting to constant frame rate..."); + + while (currentTargetTime <= inputTimestamps.Last()) // Use ^1 to get the last element + { + switch (roundMethod) + { + case Enums.Round.Near: // Find the closest timestamp to the current target time + while (index < inputTimestamps.Count - 1 && Math.Abs(inputTimestamps[index + 1] - currentTargetTime) < Math.Abs(inputTimestamps[index] - currentTargetTime)) index++; + break; + case Enums.Round.Down: // Find the closest timestamp that is <= the current target time + while (index < inputTimestamps.Count - 1 && inputTimestamps[index + 1] <= currentTargetTime) index++; + break; + case Enums.Round.Up: // Find the closest timestamp that is >= the current target time + while (index < inputTimestamps.Count - 1 && inputTimestamps[index] < currentTargetTime) index++; + break; + } + + // Debug message to show which frame index was picked + Console.WriteLine($"--> Frame {(outputTimestamps.Count + 1).ToString().PadLeft(3)} | Target Time: {(currentTargetTime * 1000f):F5} | Picked Input Index: {index} | Input TS: {(inputTimestamps[index] * 1000f):F3}"); + + // Add the closest timestamp to the output list, along with the index of the input timestamp + outputTimestamps.Add(new float[] { inputTimestamps[index], index }); + + // Move to the next frame time in the target frame rate + currentTargetTime += targetFrameInterval; + } + + return outputTimestamps; + } + } +} diff --git a/CodeLegacy/Os/VapourSynthUtils.cs b/CodeLegacy/Os/VapourSynthUtils.cs index c09e7508..d7d4c1f1 100644 --- a/CodeLegacy/Os/VapourSynthUtils.cs +++ b/CodeLegacy/Os/VapourSynthUtils.cs @@ -172,6 +172,15 @@ public static string CreateScript(VsSettings s, bool alwaysPreferFactorOverFps = l.Add(Debugger.IsAttached ? $"clip = core.text.FrameNum(clip, alignment=9, scale={txtScale}) # Output frame counter" : ""); l.Add(Debugger.IsAttached ? $"clip = core.text.Text(clip, f\"Frames: {{srcFrames}}/{{preInterpFrames}} -> {{len(clip)}} [{factor.GetString()}x]\", alignment=8, scale={txtScale})" : ""); l.Add(Debugger.IsAttached ? $"clip = core.text.Text(clip, f\"targetMatchDuration: {targetFrameCountMatchDuration} - targetTrue: {targetFrameCountTrue} - endDupeCount: {endDupeCount}\", alignment=2, scale={txtScale})" : ""); + + if(Interpolate.currentMediaFile.IsVfr && Interpolate.currentMediaFile.OutputFrameIndexes != null && Interpolate.currentMediaFile.OutputFrameIndexes.Count > 0 && s.InterpSettings.outFpsResampled.Float > 0.1f) + { + l.Add($"# Frames picked to resample VFR video to {s.InterpSettings.outFpsResampled.Float} FPS"); + l.Add($"frameIndexes = [{string.Join(", ", Interpolate.currentMediaFile.OutputFrameIndexes)}]"); + l.Add($"clip = core.std.Splice([clip[i] for i in frameIndexes if i < len(clip)])"); + l.Add($"clip = core.std.AssumeFPS(clip, fpsnum={s.InterpSettings.outFpsResampled.Numerator}, fpsden={s.InterpSettings.outFpsResampled.Denominator})"); + } + l.Add($"clip.set_output()"); // Set output l.Add(""); diff --git a/CodeLegacy/Ui/ControlExtensions.cs b/CodeLegacy/Ui/ControlExtensions.cs index 4af5a70a..fe18f4ce 100644 --- a/CodeLegacy/Ui/ControlExtensions.cs +++ b/CodeLegacy/Ui/ControlExtensions.cs @@ -61,5 +61,10 @@ public static void SetVisible(this Control control, bool visible) control.Visible = false; } } + + public static void Invoke(this Control control, MethodInvoker action) + { + control.Invoke(action); + } } } diff --git a/CodeLegacy/Ui/InterpolationProgress.cs b/CodeLegacy/Ui/InterpolationProgress.cs index d09876b3..549a14db 100644 --- a/CodeLegacy/Ui/InterpolationProgress.cs +++ b/CodeLegacy/Ui/InterpolationProgress.cs @@ -136,7 +136,7 @@ public static void UpdateInterpProgress(int frames, int target, string latestFra if (I.canceled) return; interpolatedInputFramesCount = ((frames / I.currentSettings.interpFactor).RoundToInt() - 1); //ResumeUtils.Save(); - target = (target / Interpolate.InterpProgressMultiplier).RoundToInt(); + target = (target / I.InterpProgressMultiplier).RoundToInt(); frames = frames.Clamp(0, target); if (_framesAtTime == null)