diff --git a/FileFlows.Plugin.dll b/FileFlows.Plugin.dll index 93b9e8e3..5dda64fb 100644 Binary files a/FileFlows.Plugin.dll and b/FileFlows.Plugin.dll differ diff --git a/FileFlows.Plugin.pdb b/FileFlows.Plugin.pdb index 91dfda9b..361d0de7 100644 Binary files a/FileFlows.Plugin.pdb and b/FileFlows.Plugin.pdb differ diff --git a/VideoNodes/FfmpegBuilderNodes/Audio/FfmpegBuilderAudioNormalization.cs b/VideoNodes/FfmpegBuilderNodes/Audio/FfmpegBuilderAudioNormalization.cs index d9cf8bc8..327f39d9 100644 --- a/VideoNodes/FfmpegBuilderNodes/Audio/FfmpegBuilderAudioNormalization.cs +++ b/VideoNodes/FfmpegBuilderNodes/Audio/FfmpegBuilderAudioNormalization.cs @@ -1,27 +1,46 @@ -using FileFlows.VideoNodes.FfmpegBuilderNodes.Models; -using System.Diagnostics.CodeAnalysis; -using System.Text.Json; +using System.Text.Json; namespace FileFlows.VideoNodes.FfmpegBuilderNodes; +/// +/// Flow element that normalizes the audio +/// public class FfmpegBuilderAudioNormalization : FfmpegBuilderNode { + /// public override string HelpUrl => "https://fileflows.com/docs/plugins/video-nodes/ffmpeg-builder/audio-normalization"; + /// public override string Icon => "fas fa-volume-up"; + /// public override int Outputs => 2; - + + /// + /// Gets or sets if all audio should be normalised + /// [Boolean(1)] public bool AllAudio { get; set; } + /// + /// Gets or sets if the audio should be normalised using two passes or if false, a single pass + /// [Boolean(2)] public bool TwoPass { get; set; } + /// + /// Gets or sets the pattern to match against the audio file + /// [TextVariable(3)] public string Pattern { get; set; } + /// + /// Gets or sets if the match should be inversed + /// [Boolean(4)] public bool NotMatching { get; set; } + /// + /// The loud norm target + /// internal const string LOUDNORM_TARGET = "I=-24:LRA=7:TP=-2.0"; /// @@ -87,45 +106,47 @@ public class FfmpegBuilderAudioNormalization : FfmpegBuilderNode return normalizing ? 1 : 2; } + /// + /// Do a two pass normalization against a file + /// + /// the encoding node + /// the node parameters + /// the FFmpeg executable + /// the audio index in the file + /// the local filename of the file + /// the result of the normalization public static Result DoTwoPass(EncodingNode node, NodeParameters args, string ffmpegExe, int audioIndex, string localFile) { //-af loudnorm=I=-24:LRA=7:TP=-2.0" - string output; - var result = node.Encode(args, ffmpegExe, new List - { + var result = node.Encode(args, ffmpegExe, [ "-hide_banner", "-i", localFile, - "-strict", "-2", // allow experimental stuff + "-strict", "-2", // allow experimental stuff "-map", "0:a:" + audioIndex, "-af", "loudnorm=" + LOUDNORM_TARGET + ":print_format=json", "-f", "null", "-" - }, out output, updateWorkingFile: false, dontAddInputFile: true); + ], out var output, updateWorkingFile: false, dontAddInputFile: true); if (result == false) return Result.Fail("Failed to process audio track"); - int index = output.LastIndexOf("{", StringComparison.Ordinal); - if (index == -1) + var loudNorm = ExtractParsedLoudnormJson(args.Logger, output); + + if (loudNorm.Count == 0) + { + args.Logger?.WLog("No LoudNormStats found in:\n" + output); return Result.Fail("Failed to detected json in output"); - - string json = output[index..]; - json = json.Substring(0, json.IndexOf("}", StringComparison.Ordinal) + 1); - if (string.IsNullOrEmpty(json)) - return Result.Fail("Failed to parse TwoPass json"); - LoudNormStats? stats; - try - { - stats = JsonSerializer.Deserialize(json); - } - catch (Exception ex) - { - args.Logger.ELog("Failed to parse JSON: " +ex.Message); - args.Logger.ELog("JSON:" + json); - return Result.Fail("Failed to parse JSON output from FFmpeg"); } - if (stats.input_i == "-inf" || stats.input_lra == "-inf" || stats.input_tp == "-inf" || stats.input_thresh == "-inf" || stats.target_offset == "-inf") + LoudNormStats? stats = loudNorm.FirstOrDefault(x => + { + if (x.input_i == "-inf" || x.input_lra == "-inf" || x.input_tp == "-inf" || x.input_thresh == "-inf" || + x.target_offset == "-inf") + return false; + return true; + }); + if (stats == null) { args.Logger?.WLog("-inf detected in loud norm two pass, falling back to single pass loud norm"); return $"loudnorm={LOUDNORM_TARGET}"; @@ -135,6 +156,41 @@ public class FfmpegBuilderAudioNormalization : FfmpegBuilderNode return ar; } + /// + /// Extracts Loud Norm Ststs object following the [Parsed_loudnorm log entries from the provided log data. + /// + /// The logger to use + /// The log as a string. + /// A list of Loud Norm Stats objects. + static List ExtractParsedLoudnormJson(ILogger logger, string log) + { + List results = new (); + Regex regex = new Regex(@"\[Parsed_loudnorm.*?\]\s*{(.*?)}", RegexOptions.Singleline); + MatchCollection matches = regex.Matches(log); + + foreach (Match match in matches) + { + string json = "{" + match.Groups[1].Value + "}"; + try + { + var ln = JsonSerializer.Deserialize(json); + if (ln != null) + results.Add(ln); + } + catch (Exception ex) + { + // Ignored + logger?.ELog("Failed to parse JSON: " + ex.Message); + logger?.ELog("JSON:" + json); + } + } + + return results; + } + + /// + /// Represents the loudness normalization statistics. + /// private class LoudNormStats { /* @@ -151,11 +207,31 @@ public class FfmpegBuilderAudioNormalization : FfmpegBuilderNode "target_offset" : "0.25" } */ - public string input_i { get; set; } - public string input_tp { get; set; } - public string input_lra { get; set; } - public string input_thresh { get; set; } - public string target_offset { get; set; } + + /// + /// Integrated loudness of the input in LUFS. + /// + public string input_i { get; init; } + + /// + /// True peak of the input in dBTP. + /// + public string input_tp { get; init; } + + /// + /// Loudness range of the input in LU. + /// + public string input_lra { get; init; } + + /// + /// Threshold of the input in LUFS. + /// + public string input_thresh { get; init; } + + /// + /// Target offset for normalization in LU. + /// + public string target_offset { get; init; } } } diff --git a/VideoNodes/Tests/FfmpegBuilderTests/FfmpegBuilder_BasicTests.cs b/VideoNodes/Tests/FfmpegBuilderTests/FfmpegBuilder_BasicTests.cs index b5e2d2aa..07d72e91 100644 --- a/VideoNodes/Tests/FfmpegBuilderTests/FfmpegBuilder_BasicTests.cs +++ b/VideoNodes/Tests/FfmpegBuilderTests/FfmpegBuilder_BasicTests.cs @@ -261,14 +261,12 @@ public class FfmpegBuilder_BasicTests : TestBase [TestMethod] public void FfmpegBuilder_AddAc3Aac_Normalize() { - const string file = @"D:\videos\unprocessed\dummy.mkv"; - var logger = new TestLogger(); - const string ffmpeg = @"C:\utils\ffmpeg\ffmpeg.exe"; - var vi = new VideoInfoHelper(ffmpeg, logger); + const string file = @"/home/john/src/ff-files/test-files/videos/basic {tvdb-71470}.mkv"; + var vi = new VideoInfoHelper(FfmpegPath, Logger); var vii = vi.Read(file); - var args = new NodeParameters(file, logger, false, string.Empty, null); - args.GetToolPathActual = (string tool) => ffmpeg; - args.TempPath = @"D:\videos\temp"; + var args = new NodeParameters(file, Logger, false, string.Empty, new LocalFileService()); + args.GetToolPathActual = (string tool) => FfmpegPath; + args.TempPath = @"/home/john/src/ff-files/temp"; args.Parameters.Add("VideoInfo", vii); @@ -299,7 +297,7 @@ public class FfmpegBuilder_BasicTests : TestBase ffAddAudio2.Execute(args); FfmpegBuilderAudioNormalization ffAudioNormalize = new(); - ffAudioNormalize.TwoPass = false; + ffAudioNormalize.TwoPass = true; ffAudioNormalize.AllAudio = true; ffAudioNormalize.PreExecute(args); ffAudioNormalize.Execute(args); @@ -308,7 +306,6 @@ public class FfmpegBuilder_BasicTests : TestBase ffExecutor.PreExecute(args); int result = ffExecutor.Execute(args); - string log = logger.ToString(); Assert.AreEqual(1, result); } diff --git a/VideoNodes/Tests/_LocalFileService.cs b/VideoNodes/Tests/_LocalFileService.cs index a0488592..cc33cc82 100644 --- a/VideoNodes/Tests/_LocalFileService.cs +++ b/VideoNodes/Tests/_LocalFileService.cs @@ -377,7 +377,27 @@ public class LocalFileService : IFileService public Result DirectorySize(string path) { - throw new NotImplementedException(); + if (string.IsNullOrWhiteSpace(path)) + return 0; + + if (File.Exists(path)) + path = new FileInfo(path).Directory?.FullName ?? string.Empty; + + if (string.IsNullOrWhiteSpace(path)) + return 0; + + if (Directory.Exists(path) == false) + return 0; + + try + { + DirectoryInfo dir = new DirectoryInfo(path); + return dir.EnumerateFiles("*.*", SearchOption.AllDirectories).Sum(x => x.Length); + } + catch (Exception) + { + return 0; + } } public Result SetCreationTimeUtc(string path, DateTime date)