mirror of
https://github.com/revenz/FileFlowsPlugins.git
synced 2025-12-30 18:40:29 -06:00
FF-1688: Fixed audio normalize when file name has {
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Flow element that normalizes the audio
|
||||
/// </summary>
|
||||
public class FfmpegBuilderAudioNormalization : FfmpegBuilderNode
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override string HelpUrl => "https://fileflows.com/docs/plugins/video-nodes/ffmpeg-builder/audio-normalization";
|
||||
/// <inheritdoc />
|
||||
public override string Icon => "fas fa-volume-up";
|
||||
/// <inheritdoc />
|
||||
public override int Outputs => 2;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets if all audio should be normalised
|
||||
/// </summary>
|
||||
[Boolean(1)]
|
||||
public bool AllAudio { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets if the audio should be normalised using two passes or if false, a single pass
|
||||
/// </summary>
|
||||
[Boolean(2)]
|
||||
public bool TwoPass { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the pattern to match against the audio file
|
||||
/// </summary>
|
||||
[TextVariable(3)]
|
||||
public string Pattern { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets if the match should be inversed
|
||||
/// </summary>
|
||||
[Boolean(4)]
|
||||
public bool NotMatching { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The loud norm target
|
||||
/// </summary>
|
||||
internal const string LOUDNORM_TARGET = "I=-24:LRA=7:TP=-2.0";
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -87,45 +106,47 @@ public class FfmpegBuilderAudioNormalization : FfmpegBuilderNode
|
||||
return normalizing ? 1 : 2;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Do a two pass normalization against a file
|
||||
/// </summary>
|
||||
/// <param name="node">the encoding node</param>
|
||||
/// <param name="args">the node parameters</param>
|
||||
/// <param name="ffmpegExe">the FFmpeg executable</param>
|
||||
/// <param name="audioIndex">the audio index in the file</param>
|
||||
/// <param name="localFile">the local filename of the file</param>
|
||||
/// <returns>the result of the normalization</returns>
|
||||
public static Result<string> 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<string>
|
||||
{
|
||||
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<string>.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<string>.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<string>.Fail("Failed to parse TwoPass json");
|
||||
LoudNormStats? stats;
|
||||
try
|
||||
{
|
||||
stats = JsonSerializer.Deserialize<LoudNormStats>(json);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
args.Logger.ELog("Failed to parse JSON: " +ex.Message);
|
||||
args.Logger.ELog("JSON:" + json);
|
||||
return Result<string>.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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts Loud Norm Ststs object following the [Parsed_loudnorm log entries from the provided log data.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger to use</param>
|
||||
/// <param name="log">The log as a string.</param>
|
||||
/// <returns>A list of Loud Norm Stats objects.</returns>
|
||||
static List<LoudNormStats> ExtractParsedLoudnormJson(ILogger logger, string log)
|
||||
{
|
||||
List<LoudNormStats> 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<LoudNormStats>(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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents the loudness normalization statistics.
|
||||
/// </summary>
|
||||
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; }
|
||||
|
||||
/// <summary>
|
||||
/// Integrated loudness of the input in LUFS.
|
||||
/// </summary>
|
||||
public string input_i { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// True peak of the input in dBTP.
|
||||
/// </summary>
|
||||
public string input_tp { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Loudness range of the input in LU.
|
||||
/// </summary>
|
||||
public string input_lra { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Threshold of the input in LUFS.
|
||||
/// </summary>
|
||||
public string input_thresh { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Target offset for normalization in LU.
|
||||
/// </summary>
|
||||
public string target_offset { get; init; }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -377,7 +377,27 @@ public class LocalFileService : IFileService
|
||||
|
||||
public Result<long> 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<bool> SetCreationTimeUtc(string path, DateTime date)
|
||||
|
||||
Reference in New Issue
Block a user