using FileFlows.Plugin; using System.Diagnostics.CodeAnalysis; using System.Text.Json; namespace FileFlows.AudioNodes; public class AudioFileNormalization : AudioNode { public override int Inputs => 1; public override int Outputs => 1; public override FlowElementType Type => FlowElementType.Process; public override string HelpUrl => "https://fileflows.com/docs/plugins/audio-nodes/audio-file-normalization"; public override string Icon => "fas fa-volume-up"; const string LOUDNORM_TARGET = "I=-24:LRA=7:TP=-2.0"; public override int Execute(NodeParameters args) { try { var ffmpegExeResult = GetFFmpeg(args); if (ffmpegExeResult.Failed(out string ffmpegError)) { args.FailureReason = ffmpegError; args.Logger?.ELog(ffmpegError); return -1; } string ffmpegExe = ffmpegExeResult.Value; var audioInfoResult = GetAudioInfo(args); if (audioInfoResult.Failed(out string error)) { args.Logger?.ELog(error); args.FailureReason = error; return -1; } AudioInfo AudioInfo = audioInfoResult.Value; List ffArgs = new List(); long sampleRate = AudioInfo.Frequency > 0 ? AudioInfo.Frequency : 48_000; var twoPass = DoTwoPass(args, ffmpegExe, LocalWorkingFile); if (twoPass.Success == false) { args.Logger?.WLog("Failed to normalize audio, skipping"); return 1; } ffArgs.AddRange(new[] { "-i", args.WorkingFile, "-c:a", AudioInfo.Codec, "-ar", sampleRate.ToString(), "-af", twoPass.Normalization }); string extension = FileHelper.GetExtension(args.WorkingFile); string outputFile = FileHelper.Combine(args.TempPath, Guid.NewGuid() + extension); ffArgs.Add(outputFile); var result = args.Execute(new ExecuteArgs { Command = ffmpegExe, ArgumentList = ffArgs.ToArray() }); return result.ExitCode == 0 ? 1 : -1; } catch (Exception ex) { args.Logger?.ELog("Failed processing AudioFile: " + ex.Message); return -1; } } [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] public static (bool Success, string Normalization) DoTwoPass(NodeParameters args, string ffmpegExe, string localFile) { //-af loudnorm=I=-24:LRA=7:TP=-2.0" var result = args.Execute(new ExecuteArgs { Command = ffmpegExe, ArgumentList = new[] { "-hide_banner", "-i", localFile, "-af", "loudnorm=" + LOUDNORM_TARGET + ":print_format=json", "-f", "null", "-" } }); if (result.ExitCode != 0) { args.Logger?.WLog("Failed to process audio track"); return (false, string.Empty); } string output = result.StandardOutput; int index = output.LastIndexOf("{"); if (index == -1) { args.Logger?.WLog("Failed to detected json in output"); return (false, string.Empty); } string json = output.Substring(index); json = json.Substring(0, json.IndexOf("}") + 1); if (string.IsNullOrEmpty(json)) { args.Logger?.WLog("Failed to parse TwoPass json\""); return (false, string.Empty); } LoudNormStats stats = JsonSerializer.Deserialize(json); string ar = $"loudnorm=print_format=summary:linear=true:{LOUDNORM_TARGET}:measured_I={stats.input_i}:measured_LRA={stats.input_lra}:measured_tp={stats.input_tp}:measured_thresh={stats.input_thresh}:offset={stats.target_offset}"; return (true, ar); } private class LoudNormStats { /* { "input_i" : "-7.47", "input_tp" : "12.33", "input_lra" : "6.70", "input_thresh" : "-18.13", "output_i" : "-24.25", "output_tp" : "-3.60", "output_lra" : "5.90", "output_thresh" : "-34.74", "normalization_type" : "dynamic", "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; } } }