using System.ComponentModel; using FileFlows.AudioNodes.Helpers; namespace FileFlows.AudioNodes { public abstract class ConvertNode:AudioNode { /// /// Gets the default extension to use if none set /// protected abstract string DefaultExtension { get; } /// /// Gets or sets if using high efficiency /// protected bool HighEfficiency { get; set; } /// /// Gets the bitrate of the source file /// /// the node parameters /// the source bitrate protected long GetSourceBitrate(NodeParameters args) { var info = GetAudioInfo(args).Value; return info.Bitrate; } /// /// Gets the channels of the source file /// /// the node parameters /// the source channels protected long GetSourceChannels(NodeParameters args) { var info = GetAudioInfo(args).Value; return info.Channels; } public override int Inputs => 1; public override int Outputs => 2; protected virtual List GetArguments(NodeParameters args, out string? extension) { string Codec = DefaultExtension; extension = null; string codecKey = Codec + "_codec"; string codec = args.GetToolPathActual(codecKey)?.EmptyAsNull() ?? Codec; if (codec.ToLowerInvariant() == "mp3") { extension = "mp3"; codec = "mp3"; } else if (codec == "libopus") extension = "ogg"; else if (codec == "libvorbis" || codec == "ogg") { codec = "libvorbis"; extension = "ogg"; } bool ogg = extension?.ToLowerInvariant() == "ogg"; if (codec == codecKey || string.IsNullOrWhiteSpace(codec)) { codec = Codec switch { "ogg" => "libvorbis", "wav" => "pcm_s16le", _ => Codec.ToLower() }; } int bitrate = Bitrate; List ffArgs = new() { "-c:a", codec, }; if(bitrate is > 10 and <= 20) { bool mp3 = Codec.Equals("mp3", StringComparison.InvariantCultureIgnoreCase); bool aac = Codec.Equals("aac", StringComparison.InvariantCultureIgnoreCase); if(mp3 == false && aac == false && ogg == false) throw new Exception("Variable bitrate not supported in codec: " + Codec); bitrate = (Bitrate - 10); if (mp3) { // ogg is reversed bitrate = 10 - bitrate; } args.Logger?.ILog($"Using variable bitrate setting '{bitrate}' for codec '{Codec}'"); if (codec == "libfdk_aac") { ffArgs.AddRange(new[] { "-vbr", Math.Min(Math.Max(1, bitrate / 2), 5).ToString() }); } else { ffArgs.AddRange(new[] { "-qscale:a", bitrate.ToString() }); } if (Codec == "aac" && HighEfficiency) { extension = "m4a"; if(Channels is > 0 and <= 2) ffArgs.AddRange(new[] { "-profile:a", "aac_he_v2" }); else if(Channels > 0) ffArgs.AddRange(new[] { "-profile:a", "aac_he" }); else if(GetSourceChannels(args) <= 2) ffArgs.AddRange(new[] { "-profile:a", "aac_he_v2" }); else ffArgs.AddRange(new[] { "-profile:a", "aac_he" }); } } else if (bitrate != 0) { ffArgs.AddRange(new [] { "-ab", (bitrate == -1 ? GetSourceBitrate(args).ToString() : bitrate + "k") }); if (Codec == "aac" && HighEfficiency) { extension = "m4a"; if(Channels is > 0 and <= 2) ffArgs.AddRange(new[] { "-profile:a", "aac_he_v2" }); else if(Channels > 0) ffArgs.AddRange(new[] { "-profile:a", "aac_he" }); else if(GetSourceChannels(args) <= 2) ffArgs.AddRange(new[] { "-profile:a", "aac_he_v2" }); else ffArgs.AddRange(new[] { "-profile:a", "aac_he" }); } } if (SampleRate > 0) { ffArgs.Add("-ar"); ffArgs.Add(SampleRate.ToString()); } if (Channels > 0) { ffArgs.Add("-ac"); ffArgs.Add(Channels.ToString()); } return ffArgs; } /// /// Gets the type of flow element /// public override FlowElementType Type => FlowElementType.Process; /// /// Gets or sets the bitrate for the converted audio /// [Select(nameof(BitrateOptions), 1)] public int Bitrate { get; set; } /// /// Gets or sets the sample rate /// [DefaultValue(0)] [Select(nameof(SampleRateOptions), 2)] public int SampleRate { get; set; } private static List _SampleRateOptions; /// /// Gets the sample rate options /// public static List SampleRateOptions { get { if (_SampleRateOptions == null) { _SampleRateOptions = new List { new () { Label = "Automatic", Value = 0}, new () { Label = "Same as source", Value = 1}, new () { Label = "44100", Value = 44100 }, new () { Label = "48000", Value = 48000 }, new () { Label = "88200", Value = 88200 }, new () { Label = "96000", Value = 96000 }, new () { Label = "176400", Value = 176400 }, new () { Label = "192000", Value = 192000 } }; } return _SampleRateOptions; } } /// /// Gets or sets the number of channels rate /// [DefaultValue(0)] [Select(nameof(ChannelsOptions), 3)] public int Channels { get; set; } private static List _ChannelsOptions; /// /// Gets the channel options /// public static List ChannelsOptions { get { if (_ChannelsOptions == null) { _ChannelsOptions = new List { new () { Label = "Same as source", Value = 0}, new () { Label = "Mono", Value = 1f}, new () { Label = "Stereo", Value = 2f}, new () { Label = "5.1", Value = 6}, new () { Label = "7.1", Value = 8} }; } return _ChannelsOptions; } } /// /// Gets or sets a custom extension to override the ont to use /// [TextVariable(4)] public string CustomExtension { get; set; } /// /// Gets or sets if the audio should be normalized /// [Boolean(5)] public bool Normalize { get; set; } /// /// Gets or sets if it should be skipped if the codec is the same /// [Boolean(6)] [ConditionEquals(nameof(Normalize), true, inverse: true)] public bool SkipIfCodecMatches { get; set; } private static List _BitrateOptions; /// /// Gets the bitrate options to show to the user /// public static List BitrateOptions { get { if (_BitrateOptions == null) { _BitrateOptions = new List { new () { Label = "Automatic", Value = 0 }, new () { Label = "Same as source", Value = -1 }, new () { Label = "Constant Bitrate", Value = "###GROUP###" }, new () { Label = "64 Kbps", Value = 64}, new () { Label = "96 Kbps", Value = 96}, new () { Label = "128 Kbps", Value = 128}, new () { Label = "160 Kbps", Value = 160}, new () { Label = "192 Kbps", Value = 192}, new () { Label = "224 Kbps", Value = 224}, new () { Label = "256 Kbps", Value = 256}, new () { Label = "288 Kbps", Value = 288}, new () { Label = "320 Kbps", Value = 320}, new () { Label = "Variable Bitrate", Value = "###GROUP###" }, new () { Label = "0 (Lowest Quality)", Value = 10}, new () { Label = "1", Value = 11}, new () { Label = "2", Value = 12}, new () { Label = "3", Value = 13}, new () { Label = "4", Value = 14}, new () { Label = "5 (Good Quality)", Value = 15}, new () { Label = "6", Value = 16}, new () { Label = "7", Value = 17}, new () { Label = "8", Value = 18}, new () { Label = "9", Value = 19}, new () { Label = "10 (Highest Quality)", Value = 20}, }; } return _BitrateOptions; } } /// /// Executes the flow element /// /// the node parameters /// the output to call next public override int Execute(NodeParameters args) { var aiResult = GetAudioInfo(args); if (aiResult.Failed(out string error)) { args.FailureReason = error; args.Logger.ELog(error); return -1; } AudioInfo AudioInfo = aiResult.Value; var ffmpegExeResult = GetFFmpeg(args); if (ffmpegExeResult.Failed(out string ffmpegError)) { args.FailureReason = ffmpegError; args.Logger?.ELog(ffmpegError); return -1; } string ffmpegExe = ffmpegExeResult.Value; var ffprobeResult = GetFFprobe(args); if (ffprobeResult.Failed(out string ffprobeError)) { args.FailureReason = ffprobeError; args.Logger?.ELog(ffprobeError); return -1; } string ffprobe = ffprobeResult.Value; if(Normalize == false && AudioInfo.Codec?.ToLower() == DefaultExtension?.ToLower()) { if (SkipIfCodecMatches) { args.Logger?.ILog($"Audio file already '{DefaultExtension}' at bitrate '{AudioInfo.Bitrate} bps', and set to skip if codec matches"); return 2; } args.Logger?.ILog($"Comparing bitrate {AudioInfo.Bitrate} is less than or equal to {(Bitrate * 1000)}"); if(AudioInfo.Bitrate <= Bitrate * 1000) // this bitrate is in Kbps, whereas AudioInfo.Bitrate is bytes per second { args.Logger?.ILog($"Audio file already '{DefaultExtension}' at bitrate '{AudioInfo.Bitrate} bps ({(AudioInfo.Bitrate / 1000)} KBps)'"); return 2; } if(AudioInfo.Bitrate <= Bitrate * 1024) // this bitrate is in Kbps, whereas AudioInfo.Bitrate is bytes per second { args.Logger?.ILog($"Audio file already '{DefaultExtension}' at bitrate '{AudioInfo.Bitrate} bps ({(AudioInfo.Bitrate / 1024)} KBps)'"); return 2; } } var ffArgs = GetArguments(args, out var extension); var actualExt = args.ReplaceVariables(CustomExtension, stripMissing: true)?.EmptyAsNull() ?? extension?.EmptyAsNull() ?? DefaultExtension; var outputFile = FileHelper.Combine(args.TempPath, Guid.NewGuid() + "." + actualExt.TrimStart('.')); ffArgs.Insert(0, "-hide_banner"); ffArgs.Insert(1, "-y"); // tells ffmpeg to replace the file if already exists, which it shouldnt but just incase ffArgs.Insert(2, "-i"); ffArgs.Insert(3, LocalWorkingFile); ffArgs.Insert(4, "-vn"); // disables video if (Normalize) { var twoPass = AudioFileNormalization.DoTwoPass(args, ffmpegExe, LocalWorkingFile); if (twoPass.Success) { ffArgs.Add("-af"); ffArgs.Add(twoPass.Normalization); } } var metadata = MetadataHelper.GetMetadataParameters(AudioInfo); if (metadata?.Any() == true) ffArgs.AddRange(metadata); ffArgs.Add(outputFile); args.Logger?.ILog("FFArgs: " + string.Join(" ", ffArgs.Select(x => x.IndexOf(" ") > 0 ? "\"" + x + "\"" : x).ToArray())); var result = args.Execute(new ExecuteArgs { Command = ffmpegExe, ArgumentList = ffArgs.ToArray() }); if(result.ExitCode != 0) { args.Logger?.ELog("Invalid exit code detected: " + result.ExitCode); return -1; } args.SetWorkingFile(outputFile); // update the Audio file info if (ReadAudioFileInfo(args, ffmpegExe, ffprobe, args.WorkingFile)) return 1; return -1; } } }