diff --git a/Code/AudioVideo/FFmpegCommands.cs b/Code/AudioVideo/FFmpegCommands.cs index f3cb6ee..e72a18b 100644 --- a/Code/AudioVideo/FFmpegCommands.cs +++ b/Code/AudioVideo/FFmpegCommands.cs @@ -92,17 +92,17 @@ namespace Flowframes DeleteSource(inputFile); } - public static async Task FramesToVideoVfr(string framesFile, string outPath, Interpolate.OutMode outMode, float fps, AvProcess.LogMode logMode = AvProcess.LogMode.OnlyLastLine, bool isChunk = false) + public static async Task FramesToVideoConcat(string framesFile, string outPath, Interpolate.OutMode outMode, float fps, AvProcess.LogMode logMode = AvProcess.LogMode.OnlyLastLine, bool isChunk = false) { if (logMode != AvProcess.LogMode.Hidden) Logger.Log($"Encoding video..."); string encArgs = Utils.GetEncArgs(Utils.GetCodec(outMode)) + " -pix_fmt yuv420p "; if (!isChunk) encArgs += $"-movflags +faststart"; string vfrFilename = Path.GetFileName(framesFile); - string vsync = (Interpolate.current.interpFactor == 2) ? "-vsync 1" : "-vsync 2"; + //string vsync = (Interpolate.current.interpFactor == 2) ? "-vsync 1" : "-vsync 2"; string rate = fps.ToString().Replace(",", "."); string extraArgs = Config.Get("ffEncArgs"); - string args = $"{vsync} -f concat -i {vfrFilename} -r {rate} {encArgs} {extraArgs} -threads {Config.GetInt("ffEncThreads")} {outPath.Wrap()}"; + string args = $"-loglevel error -vsync 0 -f concat -r {rate} -i {vfrFilename} {encArgs} {extraArgs} -threads {Config.GetInt("ffEncThreads")} {outPath.Wrap()}"; //string args = $"-vsync 0 -f concat -i {vfrFilename} {encArgs} {extraArgs} -threads {Config.GetInt("ffEncThreads")} {outPath.Wrap()}"; await AvProcess.RunFfmpeg(args, framesFile.GetParentDir(), logMode); } @@ -183,7 +183,7 @@ namespace Flowframes public static async Task Encode(string inputFile, string vcodec, string acodec, int crf, int audioKbps = 0, bool delSrc = false) { string outPath = Path.ChangeExtension(inputFile, null) + "-convert.mp4"; - string args = $" -i {inputFile.Wrap()} -c:v {vcodec} -crf {crf} -pix_fmt yuv420p -c:a {acodec} -b:a {audioKbps}k {outPath.Wrap()}"; + string args = $" -i {inputFile.Wrap()} -c:v {vcodec} -crf {crf} -pix_fmt yuv420p -c:a {acodec} -b:a {audioKbps}k -vf {divisionFilter} {outPath.Wrap()}"; if (string.IsNullOrWhiteSpace(acodec)) args = args.Replace("-c:a", "-an"); if (audioKbps < 0) diff --git a/Code/ExtensionMethods.cs b/Code/ExtensionMethods.cs index 29b5b11..8748cd2 100644 --- a/Code/ExtensionMethods.cs +++ b/Code/ExtensionMethods.cs @@ -155,5 +155,15 @@ namespace Flowframes return str.Remove(place, stringToReplace.Length).Insert(place, replaceWith); } + + public static string[] SplitBy (this string str, string splitBy) + { + return str.Split(new string[] { splitBy }, StringSplitOptions.None); + } + + public static string RemoveComments (this string str) + { + return str.Split('#')[0].SplitBy("//")[0]; + } } } diff --git a/Code/Magick/MagickDedupe.cs b/Code/Magick/MagickDedupe.cs index b3c3303..3a97ccc 100644 --- a/Code/Magick/MagickDedupe.cs +++ b/Code/Magick/MagickDedupe.cs @@ -81,6 +81,9 @@ namespace Flowframes.Magick bool hasReachedEnd = false; + string infoFile = Path.Combine(path.GetParentDir(), $"dupes.ini"); + string fileContent = ""; + for (int i = 0; i < framePaths.Length; i++) // Loop through frames { if (hasReachedEnd) @@ -133,16 +136,17 @@ namespace Flowframes.Magick } else { + fileContent += $"{Path.GetFileNameWithoutExtension(framePaths[i].Name)}:{currentDupeCount}\n"; statsFramesKept++; currentOutFrame++; currentDupeCount = 0; break; } - if (sw.ElapsedMilliseconds >= 1000 || (i+1) == framePaths.Length) + if (sw.ElapsedMilliseconds >= 1000 || (i+1) == framePaths.Length) // Print every 1s (or when done) { sw.Restart(); - Logger.Log($"[FrameDedup] Difference from {Path.GetFileName(frame1)} to {Path.GetFileName(frame2)}: {diff.ToString("0.00")}% - {delStr}. Total: {framePaths.Length - statsFramesDeleted} kept / {statsFramesDeleted} deleted.", false, true); + Logger.Log($"[FrameDedup] Difference from {Path.GetFileName(frame1)} to {Path.GetFileName(frame2)}: {diff.ToString("0.00")}% - {delStr}.", false, true); Program.mainForm.SetProgress((int)Math.Round(((float)i / framePaths.Length) * 100f)); if (imageCache.Count > bufferSize || (imageCache.Count > 50 && OSUtils.GetFreeRamMb() < 2500)) ClearCache(); @@ -169,6 +173,8 @@ namespace Flowframes.Magick } } + File.WriteAllText(infoFile, fileContent); + foreach (string frame in framesToDelete) IOUtils.TryDeleteIfExists(frame); @@ -176,10 +182,15 @@ namespace Flowframes.Magick if (Interpolate.canceled) return; + int framesLeft = IOUtils.GetAmountOfFiles(path, false, $"*.png"); + int framesDeleted = framePaths.Length - framesLeft; + float percentDeleted = ((float)framesDeleted / framePaths.Length) * 100f; + string keptPercent = $"{(100f - percentDeleted).ToString("0.0")}%"; + if (skipped) Logger.Log($"[FrameDedup] First {skipAfterNoDupesFrames} frames did not have any duplicates - Skipping the rest!", false, true); else - Logger.Log($"[FrameDedup]{testStr} Done. Kept {statsFramesKept} frames, deleted {statsFramesDeleted} frames.", false, true); + Logger.Log($"[FrameDedup]{testStr} Done. Kept {framesLeft} ({keptPercent}) frames, deleted {framesDeleted} frames.", false, true); if (statsFramesKept <= 0) Interpolate.Cancel("No frames were left after de-duplication!\n\nTry decreasing the de-duplication threshold."); diff --git a/Code/Main/CreateVideo.cs b/Code/Main/CreateVideo.cs index beeb460..80befad 100644 --- a/Code/Main/CreateVideo.cs +++ b/Code/Main/CreateVideo.cs @@ -112,7 +112,7 @@ namespace Flowframes.Main bool h265 = Config.GetInt("mp4Enc") == 1; int crf = h265 ? Config.GetInt("h265Crf") : Config.GetInt("h264Crf"); - await FFmpegCommands.FramesToVideoVfr(vfrFile, outPath, mode, fps); + await FFmpegCommands.FramesToVideoConcat(vfrFile, outPath, mode, fps); await MergeAudio(i.current.inPath, outPath); if (changeFps > 0) @@ -185,7 +185,7 @@ namespace Flowframes.Main string vfrFile = Path.Combine(i.current.tempFolder, $"vfr-chunk-{firstFrameNum}-{firstFrameNum + framesAmount}.ini"); File.WriteAllLines(vfrFile, IOUtils.ReadLines(vfrFileOriginal).Skip(firstFrameNum * 2).Take(framesAmount * 2)); - await FFmpegCommands.FramesToVideoVfr(vfrFile, outPath, mode, i.current.outFps, AvProcess.LogMode.Hidden, true); + await FFmpegCommands.FramesToVideoConcat(vfrFile, outPath, mode, i.current.outFps, AvProcess.LogMode.Hidden, true); } static async Task Loop(string outPath, int looptimes) diff --git a/Code/Main/FrameTiming.cs b/Code/Main/FrameTiming.cs index 829d0a9..7b13187 100644 --- a/Code/Main/FrameTiming.cs +++ b/Code/Main/FrameTiming.cs @@ -14,13 +14,24 @@ namespace Flowframes.Main { class FrameTiming { + public enum Mode { CFR, VFR } public static int timebase = 10000; - public static async Task CreateTimecodeFiles(string framesPath, bool loopEnabled, int times, bool noTimestamps) + public static async Task CreateTimecodeFiles(string framesPath, Mode mode, bool loopEnabled, int times, bool noTimestamps) { Logger.Log("Generating timecodes..."); - await CreateTimecodeFile(framesPath, loopEnabled, times, false, noTimestamps); - Logger.Log($"Generating timecodes... Done.", false, true); + try + { + if (mode == Mode.VFR) + await CreateTimecodeFile(framesPath, loopEnabled, times, false, noTimestamps); + if (mode == Mode.CFR) + await CreateEncFile(framesPath, loopEnabled, times, false); + Logger.Log($"Generating timecodes... Done.", false, true); + } + catch (Exception e) + { + Logger.Log($"Error generating timecodes: {e.Message}"); + } } public static async Task CreateTimecodeFile(string framesPath, bool loopEnabled, int interpFactor, bool notFirstRun, bool noTimestamps) @@ -138,5 +149,124 @@ namespace Flowframes.Main File.Copy(frameFiles.First().FullName, loopFrameTargetPath); } } + + static Dictionary dupesDict = new Dictionary(); + + static void LoadDupesFile (string path) + { + if (!File.Exists(path)) return; + dupesDict.Clear(); + string[] dupesFileLines = IOUtils.ReadLines(path); + foreach(string line in dupesFileLines) + { + string[] values = line.Split(':'); + dupesDict.Add(values[0], values[1].GetInt()); + } + } + + public static async Task CreateEncFile (string framesPath, bool loopEnabled, int interpFactor, bool notFirstRun) + { + if (Interpolate.canceled) return; + Logger.Log($"Generating timecodes for {interpFactor}x...", false, true); + + bool loop = Config.GetBool("enableLoop"); + bool sceneDetection = true; + string ext = InterpolateUtils.GetOutExt(); + + FileInfo[] frameFiles = new DirectoryInfo(framesPath).GetFiles($"*.png"); + string vfrFile = Path.Combine(framesPath.GetParentDir(), $"vfr-{interpFactor}x.ini"); + string fileContent = ""; + string dupesFile = Path.Combine(framesPath.GetParentDir(), $"dupes.ini"); + LoadDupesFile(dupesFile); + + string scnFramesPath = Path.Combine(framesPath.GetParentDir(), Paths.scenesDir); + string interpPath = Paths.interpDir; + + List sceneFrames = new List(); + if (Directory.Exists(scnFramesPath)) + sceneFrames = Directory.GetFiles(scnFramesPath).Select(file => Path.GetFileNameWithoutExtension(file)).ToList(); + + int totalFileCount = 1; + for (int i = 0; i < (frameFiles.Length - 1); i++) + { + if (Interpolate.canceled) return; + + int interpFramesAmount = interpFactor; + string inputFilenameNoExt = Path.GetFileNameWithoutExtension(frameFiles[i].Name); + int dupesAmount = dupesDict.ContainsKey(inputFilenameNoExt) ? dupesDict[inputFilenameNoExt] : 0; + //Logger.Log($"{Path.GetFileNameWithoutExtension(frameFiles[i].Name)} has {dupesAmount} dupes", true); + + bool discardThisFrame = (sceneDetection && (i + 2) < frameFiles.Length && sceneFrames.Contains(Path.GetFileNameWithoutExtension(frameFiles[i + 1].Name))); // i+2 is in scene detection folder, means i+1 is ugly interp frame + + // TODO: Check if this is needed + // If loop is enabled, account for the extra frame added to the end for loop continuity + if (loopEnabled && i == (frameFiles.Length - 2)) + interpFramesAmount = interpFramesAmount * 2; + + //Logger.Log($"Writing out frames for in frame {i} which has {dupesAmount} dupes", true); + // Generate frames file lines + for (int frm = 0; frm < interpFramesAmount; frm++) + { + //Logger.Log($"Writing out frame {frm+1}/{interpFramesAmount}", true); + + + if (discardThisFrame && totalFileCount > 1) // If frame is scene cut frame + { + int lastNum = totalFileCount; + + //Logger.Log($"Writing frame {totalFileCount} [Discarding Next]", true); + fileContent += $"file '{interpPath}/{totalFileCount.ToString().PadLeft(Padding.interpFrames, '0')}.{ext}'\n"; + totalFileCount++; + + //Logger.Log("Discarding interp frames with out num " + totalFileCount); + for (int dupeCount = 1; dupeCount < interpFramesAmount; dupeCount++) + { + //Logger.Log($"Writing frame {totalFileCount} which is actually repeated frame {lastNum}"); + fileContent += $"file '{interpPath}/{lastNum.ToString().PadLeft(Padding.interpFrames, '0')}.{ext}'\n"; + totalFileCount++; + } + + frm = interpFramesAmount; + } + else + { + for(int writtenDupes = -1; writtenDupes < dupesAmount; writtenDupes++) // Write duplicates + { + //Logger.Log($"Writing frame {totalFileCount}", true, false); + fileContent += $"file '{interpPath}/{totalFileCount.ToString().PadLeft(Padding.interpFrames, '0')}.{ext}'\n"; + } + totalFileCount++; + } + } + + if ((i + 1) % 100 == 0) + await Task.Delay(1); + } + + // Use average frame duration for last frame - TODO: Use real duration?? + //string durationStrLast = ((totalDuration / (totalFileCount - 1)) / timebase).ToString("0.0000000", CultureInfo.InvariantCulture); + fileContent += $"file '{interpPath}/{totalFileCount.ToString().PadLeft(Padding.interpFrames, '0')}.{ext}'\n"; + totalFileCount++; + + string finalFileContent = fileContent.Trim(); + if(loop) + finalFileContent = finalFileContent.Remove(finalFileContent.LastIndexOf("\n")); + File.WriteAllText(vfrFile, finalFileContent); + + if (notFirstRun) return; // Skip all steps that only need to be done once + + if (loop) + { + int lastFileNumber = frameFiles.Last().Name.GetInt() + 1; + string loopFrameTargetPath = Path.Combine(frameFiles.First().FullName.GetParentDir(), lastFileNumber.ToString().PadLeft(Padding.inputFrames, '0') + $".png"); + if (File.Exists(loopFrameTargetPath)) + { + Logger.Log($"Won't copy loop frame - {Path.GetFileName(loopFrameTargetPath)} already exists.", true); + return; + } + File.Copy(frameFiles.First().FullName, loopFrameTargetPath); + Logger.Log($"Copied loop frame to {loopFrameTargetPath}.", true); + } + } } } diff --git a/Code/Main/Interpolate.cs b/Code/Main/Interpolate.cs index edb305a..2afafe7 100644 --- a/Code/Main/Interpolate.cs +++ b/Code/Main/Interpolate.cs @@ -125,7 +125,7 @@ namespace Flowframes if (canceled) return; bool useTimestamps = Config.GetInt("timingMode") == 1; // TODO: Auto-Disable timestamps if input frames are sequential, not timestamped - await FrameTiming.CreateTimecodeFiles(current.framesFolder, Config.GetBool("enableLoop"), current.interpFactor, !useTimestamps); + await FrameTiming.CreateTimecodeFiles(current.framesFolder, FrameTiming.Mode.CFR, Config.GetBool("enableLoop"), current.interpFactor, !useTimestamps); if (canceled) return; diff --git a/Code/Main/InterpolateUtils.cs b/Code/Main/InterpolateUtils.cs index fbc112d..ac757d6 100644 --- a/Code/Main/InterpolateUtils.cs +++ b/Code/Main/InterpolateUtils.cs @@ -34,6 +34,7 @@ namespace Flowframes.Main public static int currentFactor; public static async void GetProgressByFrameAmount(string outdir, int target) { + Logger.Log($"Starting GetProgressByFrameAmount() loop for outdir '{outdir}', target is {target} frames", true); bool firstProgUpd = true; Program.mainForm.SetProgress(0); targetFrames = target;