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)