From 879215e251b9c8ad49abebd3a998d3e7e9d4ae18 Mon Sep 17 00:00:00 2001 From: John Andrews Date: Tue, 23 Jul 2024 08:37:41 +1200 Subject: [PATCH] FF-1688: Fixed audio normalize when file name has { --- FileFlows.Plugin.dll | Bin 147968 -> 147968 bytes FileFlows.Plugin.pdb | Bin 36788 -> 36788 bytes .../Audio/FfmpegBuilderAudioNormalization.cs | 140 ++++++++++++++---- .../FfmpegBuilder_BasicTests.cs | 15 +- VideoNodes/Tests/_LocalFileService.cs | 22 ++- 5 files changed, 135 insertions(+), 42 deletions(-) diff --git a/FileFlows.Plugin.dll b/FileFlows.Plugin.dll index 93b9e8e39a8b3b3756f8e52e1cb9de4f6f81bce1..5dda64fbc5435eb99af037c5daebc4b452a82e1d 100644 GIT binary patch delta 250 zcmZo@;cRH(oY29-zJX;)V^3=jf_F|vH#?=^k8 zh$T~ifMa>eqAi{?9hbGKi%WQt|= zH)Tj=NMT3>;xqI49P%IV+I2-+ZZU52xglA`9?q;Ao)ZfnFQ2f Q2oy17NZVd-&(z2S0Fp*cJOBUy delta 250 zcmZo@;cRH(oY28y&}w#|v8T0%acd9L2PJ{E0g4BWOqY4JhM3OY`BQBF_8w)XbHW|)zzr48Av~j`b?UyZ? zVp;vo8B!Sx8H^cB8BBoKlEIw8h`|hqlfmMtK+$BNXbMoykRb^yX95&60je=(NCb*l U0%ekcy3BxVpjq4N?U@>x0N_YWmjD0& diff --git a/FileFlows.Plugin.pdb b/FileFlows.Plugin.pdb index 91dfda9b89347e3b20cd0362e71d4f2a1e9d0d17..361d0de7cf6651cf0163273733b271afa1864888 100644 GIT binary patch delta 2987 zcmZwIc~nzZ9tZH>ONaphAqAo!0x{A66-kJRNt7az$W}nYk|1i?9VlSI6{I|tTHLT| zM67k(H?UP9b|5ly#P*bqp0TwaZN;_4W2=m7aS-gZ&NuIj{>3}z^Sj^Nckf$n-aGHq z9^R=vyv@~~Q7iNJpNjh=sy@C~&{oIV2G7K|v~S+do4jX+AI;SG(Lp!{9dJoQ*!<`r z@`%R52tC~8eyB(K5!a2>86o8j0ZoRy)T>sb>TD>;$R+_U=b8R2`q&w zsDrieF>Hr+=z??Hag=mXr=%ZQlv{8Y9>5^H0A{9=9Kj95&;#GVw{Qh+z#SL?nx!Nm zi~}$51u2Ap0yGc_b0HDZAPaJVEkb!8RzeM|fsJ5+ov;sDp&e{+7S6%<&<8)meRv8Z zz(gp?2}Ixx5(owbXdw#XAO$iblq}^S6hS#uLLIDwE$|8KgH~t<8=Qr6@IBmvCol~3 zHdYMWVFCm|5XeCTvmq98p%^Nl>TOm@^$3r^Gg3(rkO|A75k7_%*aJu41XRt&#lcYs zi&9boEQFIVevXo+z%r?hLFdofz7fQvdURdRhAs*TZ5V+`KcMwY^8Q%o2&!!9@gZO{%~aE6;9r`{MjU4YAQ z9s1!uJb~x%CkSHY=Fi=B8?5kT>kUmyH3BVdrG9eolLm4dR2Fj>9Rz?l5 z9=1S#j0`80(GKK2a2TG!@4$;gKYSQ3qoM>Et$?j?9In9-%zj5ku}}tUz#~z{r`zxy{Aq~G0kp~(K($7Dzl7S15^Cr2Wa=_brkh-r zk|fHtx5*&%OmxPii2&3j+R8B9xepvZIqC4dns^a4CA0?I_rhQ;Y)s6Jgt>tHjqz}Ijco`PeBko+@* zEU6Kapb%n&r0-x|x+DFN;Y7b?jHSU0XL<=^OwRb6IOB8TOs*yunhk+E7kpM+ zCmZa4&=ffa7)-08H5b*Hl?5#2PsLHA7_^dFNa?$8r= z=t+#(i{1b!gqX#oFpsAQ&_jZG0$#2Ov=D8+*_)P|eW=NUhF|V_dHe}){_4QYiB~wkG!qL||?SpmHl%=F? z))UU9Qg#;ipfNOAgTIRSz|b0v0PPqqtF(@2ow%&T>JhncdB@CgXx+ex^aiYA3`19A zJ@DHTyucM0k|a++>kU3I7Fdp&?^7V&uwlp&I$rir9{2QrGHx!QobK=c*re38__fpSIBsM(?h zt{&j(3ZcbE{-(!Ob5FDrC1mwPkEQ&qY`qhmx2DRoJy=W9$o7!#l83E<4Vm-%LUNa^ zp1G*l;O97TY@ko;gCX)&BMV>g3HC;^;*9YI(e=Tld&*AdjZGGoL7g^@*b1ix{b^Wc2 z+O{3VW#8R=`rjWryvE z)6(88?b9b6*}wFMW>w{d5B%@kh(B@E?cy10Nz2gF;lWea|M+|5(L?8dz7n6j;p9NV z27}=m-z3TNHN4Ac7&W7^gd1{#R0a7t`Gs1QO06geSLNmvD&?wNr8-Bhnyw1Z&&|up z%~dGE6@}JrgZ@qNVW#tQrqjlBUS>LPF`bW?&X-J=*y2@^=sW7JIZRhJ)3wx6Q?hN; zxi+ySsPvx4D08jSG9wc>$_~aRX1XUa-72O#jp;66x@)XEmoz%L9=4wfSJx%RMOmj+ z4l#__8n}8h<2~9zFx@7$ep@XcGjX)tJ(TQXAK=|^*TDe;Y*yY{H@=fSF+F6GT75R` Ibif<^KUk(0QUCw| delta 2995 zcmZwIc~nzZ9tZH>OMOO5JgS zOL3P`XvJ0ymL;AR#hIzg8At0jPEj0m)Y3CLTB~U7neTgN^e^5ypWpqy`|jIr?mKBe zz-vFiYp8YG?9!L;@|!ntyDPFc=Ty#ql&b?lJikLx-ZV|&O-JE4w8M1;Ve_V^ z$Zr*9#@5XgLbl2fN{59|3magkk{uXIj;c@^4W2MT#rB1hL={eE2>t^v;Wyw_W%8t5Po3c*y0vKXpi71YC4*a3T?8IFMktk4UW;0O3848S0~ zfVaS(CL>31g>fK;$smI%5DzmU6LO}>OjLkS3QM2{>R=O?VGlIJF|dFYdf^iM06)QV zcmup>%ovP@vETwXp@ZAB0PgJF*1sQ99Rk)U^{#chu{l13ANMl zaPTEaV`Y>IbKn$s#>prEs$eC&264QMA|VB|a1c(xdB`%U@moNVI)cif5^7)#Y=8#X zrDhl2`z@08f7qAP5w)D!zyjTH7QTloa1;9A0XJSwgUHWe2q-~LwqSBbaR)CDLl8(o zk?{V8?^`*=pkK%J%P9}J^8dL7`5t&AMo}=xKn1a&fqW=}GFSr3VKwYcjG9cXi7IM` z)Fk#sN-9rMQYBndDXE6*Q_z|u1#L*e_gZhFkEkK(BiaS~;Rv*W1-juZcSaa}pA<${ z;3o9J0~mzoFa)FtBU^9=cklu+1c4M35Tkkj>QYM7NGTOeT9h0xLJ3sBQf{DxR%#@) z4mN`s9wbR{QwcR8AA(l+72bhh2Kr%rvV=-!N@y80!U^~hUPD}pgp#2G>cKr#!lzZK zn1^&zAgN~sQVM7x7Yd;aDxntE!e(fMy|dU?38X(Ew?R9cf%9+~uEUS;0DgvF;5Y7^ zAQGenQ7q&_0W5$;CX~GN$#iTsdto2CGuww=b9n+S&zL}KGT44!YRm8?3zz+EbCA4LbD1-^pQ(61UrXY{5~)TPLhuA%2m~n`ln|TEp2Th;G~+{-(UxqiQZFAl?Z}qXKKSy(L3Xf;w&loZ zr{!zMa zjVt!uz#T>cL*g_MT2B}Y4lt6$S^?L}^^A#UOWwRb67$CJBxFPLg%aHOF{&+vyTy}$ ziiHJSt>fwd>Rch8VQVw$AB23KfDA%0s!k~83rH)B=V~2lzOWZHS11+;s8AS>Y82LS zwVA6`w&t;60j_rpJ@NLGn$r{SK*c$8)%JARk{LGF)%2Fv`g{BRnrEShTZ&tETv}8q zTJXH^&7-`lgQIViOum-CTK^Mf?6^6#v^UIY{nH`$!!28`7M8q-y(xX{Fn)?ta?Urs zx6dwWJl}u2*Jtw3t0mp1+Z(rd`5g$pur1$SY4?}9CJ#YHTHE#k2b&<{iDi##_m!6( zU44-^FDb%npuyBJy+?Cl#ja{i{&lssb)QeltFH>Z4*7IN;z;Bz{ld>gd-FVIYlc&DZkITI{ep$f&8!6foC8y68S5>{ZWmtS`gxUD- z>Zj_`vc-4r=Bw6M%~PnzGIF6qXHH~VkQe^P9Du(;)&dxyKpwRv#cx07a{y4d~g zzE^_I#I5Rrg|j66?mMDe*8X0;bb03fgupDzt+>}doqODROT7M^t(pCGVhQ9n=^sM758;3 zzR=t_7&g7@*yEbJQH+9-n?n-{CPfuVWswn)%BXO8ctMf0D55}F7%4S|%OfHp%h8Q^YRik)nX{W@)mkIkYU?YuI6G zrqjrDerm2QYaBD&fG&|ap!~7xFo!I@pJXup!yI6`M2yvku|_dg9b^5Nv97Z0URZBG zvX#9Rp0rbh2ebrN|H?2Ti@2tN@f_|VIK4|`xm+7IV%%_tdnnDB9pF*-P$0kn>#5IN R`1WjIdPwhY5p8M`{0{&b3&Q{a 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)