Files
FileFlowsPlugins/VideoNodes/FfmpegBuilderNodes/Audio/FfmpegBuilderAudioConverter.cs
T
2024-07-27 10:31:09 +12:00

384 lines
13 KiB
C#

using FileFlows.VideoNodes.FfmpegBuilderNodes.Models;
using FileFlows.VideoNodes.Helpers;
namespace FileFlows.VideoNodes.FfmpegBuilderNodes;
/// <summary>
/// FFmpeg Builder flow element that converts audio
/// </summary>
public class FfmpegBuilderAudioConverter : FfmpegBuilderNode
{
/// <inheritdoc />
public override string HelpUrl => "https://fileflows.com/docs/plugins/video-nodes/ffmpeg-builder/audio-converter";
/// <inheritdoc />
public override string Icon => "fas fa-comments";
/// <inheritdoc />
public override int Outputs => 2;
/// <summary>
/// Gets or sets the codec to use
/// </summary>
[DefaultValue("aac")]
[Select(nameof(CodecOptions), 1)]
public string Codec { get; set; }
/// <summary>
/// The available codec options
/// </summary>
private static List<ListOption> _CodecOptions;
/// <summary>
/// Gets the available codec options
/// </summary>
public static List<ListOption> CodecOptions
{
get
{
if (_CodecOptions == null)
{
_CodecOptions = new List<ListOption>
{
new () { Label = "AAC", Value = "aac"},
new () { Label = "AC3", Value = "ac3"},
new () { Label = "DTS", Value = "dts" },
new () { Label = "EAC3", Value = "eac3" },
new () { Label = "FLAC", Value ="flac" },
new () { Label = "MP3", Value = "mp3"},
new () { Label = "PCM", Value ="pcm" },
new () { Label = "OPUS", Value = "opus"},
new () { Label = "Vorbis", Value ="libvorbis" },
};
}
return _CodecOptions;
}
}
/// <summary>
/// Gets or sets the PCM format
/// </summary>
[DefaultValue("pcm_s16le")]
[Select(nameof(PcmFormats), 2)]
[ConditionEquals(nameof(Codec), "pcm")]
public string PcmFormat { get; set; }
/// <summary>
/// The PCM options
/// </summary>
private static List<ListOption> _PcmFormats;
/// <summary>
/// Gets the PCM options
/// </summary>
public static List<ListOption> PcmFormats
{
get
{
if (_PcmFormats == null)
{
_PcmFormats = new List<ListOption>
{
new () { Label = "Common", Value = "###GROUP###" },
new () { Label = "Signed 16-bit Little Endian", Value = "pcm_s16le" },
new () { Label = "Signed 24-bit Little Endian", Value = "pcm_s24le"},
new () { Label = "Signed", Value = "###GROUP###" },
new () { Label = "8-bit", Value = "pcm_s8"},
new () { Label = "16-bit Little Endian", Value = "pcm_s16le" },
new () { Label = "16-bit Big Endian", Value = "pcm_s16be"},
new () { Label = "24-bit Little Endian", Value = "pcm_s24le"},
new () { Label = "24-bit Big Endian", Value = "pcm_s24be"},
new () { Label = "32-bit Little Endian", Value = "pcm_s32le"},
new () { Label = "32-bit Big Endian", Value = "pcm_s32be"},
new () { Label = "64-bit Little Endian", Value = "pcm_s64le"},
new () { Label = "64-bit Big Endian", Value = "pcm_s64be"},
new () { Label = "Floating-point", Value = "###GROUP###" },
new () { Label = "32-bit Little Endian", Value = "pcm_f32le"},
new () { Label = "32-bit Big Endian", Value = "pcm_f32be"},
new () { Label = "16-bit Little Endian", Value = "pcm_f16le"},
new () { Label = "24-bit Little Endian", Value = "pcm_f24le"},
new () { Label = "64-bit Little Endian", Value = "pcm_f64le"},
new () { Label = "64-bit Big Endian", Value = "pcm_f64be"},
};
}
return _PcmFormats;
}
}
/// <summary>
/// Gets or sets the number of channels for the converted audio
/// </summary>
[DefaultValue(0)]
[Select(nameof(ChannelsOptions), 3)]
public float Channels { get; set; }
/// <summary>
/// The channel options
/// </summary>
private static List<ListOption> _ChannelsOptions;
/// <summary>
/// Gets the channel options
/// </summary>
public static List<ListOption> ChannelsOptions
{
get
{
if (_ChannelsOptions == null)
{
_ChannelsOptions = new List<ListOption>
{
new () { Label = "Automatic", Value = 1000f},
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;
}
}
/// <summary>
/// Gets or sets the bitrate
/// </summary>
[Select(nameof(BitrateOptions), 4)]
public int Bitrate { get; set; }
/// <summary>
/// The bitrate options
/// </summary>
private static List<ListOption> _BitrateOptions;
/// <summary>
/// Gets the bitrate options
/// </summary>
public static List<ListOption> BitrateOptions
{
get
{
if (_BitrateOptions == null)
{
_BitrateOptions = new List<ListOption>
{
new () { Label = "Automatic", Value = 0},
new () { Label = "Same as source", Value = 1},
};
for (int i = 64; i <= 2048; i += 32)
{
_BitrateOptions.Add(new ListOption { Label = i + " Kbps", Value = i });
}
}
return _BitrateOptions;
}
}
/// <summary>
/// Gets or sets if the bitrate specified should be per channel
/// </summary>
[Boolean(5)]
public bool BitratePerChannel { get; set; }
/// <summary>
/// Gets or sets the field to match again
/// </summary>
[DefaultValue("")]
[Select(nameof(FieldOptions), 6)]
public string Field { get; set; }
/// <summary>
/// The field options
/// </summary>
private static List<ListOption> _FieldOptions;
/// <summary>
/// Constant for the Title field
/// </summary>
internal const string FIELD_TITLE = "Title";
/// <summary>
/// Constant for the Language field
/// </summary>
internal const string FIELD_LANGUAGE = "Language";
/// <summary>
/// Constant for the Codec field
/// </summary>
internal const string FIELD_CODEC = "Codec";
/// <summary>
/// Gets the field options
/// </summary>
public static List<ListOption> FieldOptions
{
get
{
if (_FieldOptions == null)
{
_FieldOptions = new List<ListOption>
{
new() { Label = "Convert All", Value = "" },
new() { Label = "Title", Value = FIELD_TITLE },
new() { Label = "Language", Value = FIELD_LANGUAGE },
new() { Label = "Codec", Value = FIELD_CODEC },
};
}
return _FieldOptions;
}
}
/// <summary>
/// Gets or sets the pattern to match against
/// </summary>
[TextVariable(7)]
[ConditionEquals(nameof(Field), "", true)]
public string Pattern { get; set; }
/// <summary>
/// Gets or sets if the pattern should not match
/// </summary>
[Boolean(8)]
[ConditionEquals(nameof(Field), "", true)]
public bool NotMatching { get; set; }
/// <inheritdoc />
public override int Execute(NodeParameters args)
{
bool converting = false;
Regex? regex = null;
foreach (var track in Model.AudioStreams)
{
if (track.Deleted)
{
args.Logger?.ILog($"Stream {track} is deleted, skipping");
continue;
}
bool convert = false;
if (string.IsNullOrEmpty(this.Field))
{
convert = true;
}
else
{
string? testValue = Field switch
{
FIELD_LANGUAGE => track.Language?.EmptyAsNull() ?? track.Stream?.Language ?? string.Empty,
FIELD_TITLE => track.Title?.EmptyAsNull() ?? track.Stream?.Title ?? string.Empty,
FIELD_CODEC => track.Codec?.EmptyAsNull() ?? track.Stream?.Codec ?? string.Empty,
_ => null
};
if (testValue == null)
{
args.Logger?.ILog("Failed to load test value for stream: " + track);
continue;
}
string pattern = this.Pattern ?? string.Empty;
if (GeneralHelper.IsRegex(pattern))
{
try
{
convert =
new Regex(pattern, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase).IsMatch(
testValue);
if (NotMatching)
convert = !convert;
}
catch (Exception ex)
{
args.Logger?.WLog("Invalid pattern: " + ex.Message);
continue;
}
}
else if (Field == FIELD_LANGUAGE)
{
args.Logger?.ILog("Matching language using language helper.");
convert = LanguageHelper.AreSame(pattern, testValue);
if (NotMatching)
convert = !convert;
}
else
{
convert = string.Equals(pattern, testValue, StringComparison.InvariantCultureIgnoreCase);
if (NotMatching)
convert = !convert;
}
}
if (convert == false)
{
args.Logger?.ILog("Stream does not match conditions: " + track);
continue;
}
bool convertResult = ConvertTrack(args, track);
if (convertResult)
{
args.Logger?.ILog($"Stream {track} matches pattern and will be converted");
converting = true;
}
else
{
args.Logger?.ILog($"Stream {track} matches pattern but will not be converted");
}
}
return converting ? 1 : 2;
}
/// <summary>
/// Converts and audio track
/// </summary>
/// <param name="args">the node arguments</param>
/// <param name="stream">teh stream to convert</param>
/// <returns>if the stream had to be converted or not</returns>
private bool ConvertTrack(NodeParameters args, FfmpegAudioStream stream)
{
string codec = Codec?.ToLowerInvariant() ?? string.Empty;
if (codec == "pcm")
codec = PcmFormat;
bool codecSame = stream.Stream.Codec?.ToLowerInvariant() == codec;
int originalChannels = FfmpegBuilderAudioAddTrack.GetAudioBitrateChannels(args.Logger, stream.Channels, string.Empty);
var channels = this.Channels > 999f ? 0 : this.Channels == 0 ? originalChannels : this.Channels;
int totalChannels = FfmpegBuilderAudioAddTrack.GetAudioBitrateChannels(args.Logger, channels, codec);
bool channelsSame = originalChannels == totalChannels;
int bitrate = Bitrate;
if (BitratePerChannel && bitrate > 0)
{
args.Logger?.ILog("Total channels: " + totalChannels);
args.Logger?.ILog("Bitrate Per Channel: " + bitrate);
if (bitrate == 1)
{
// same as source
// so we need to get the bitrate of the original, and divide that by the number of channels
// then times it by the number of channels we are now using
bitrate = (int)(totalChannels * ((stream.Stream.Bitrate / 1000) / stream.Stream.Channels));
}
else
{
bitrate = totalChannels * bitrate;
}
args.Logger?.ILog("Total Bitrate: " + bitrate);
}
bool bitrateSame = Bitrate < 2 || stream.Stream.Bitrate == 0 ||
Math.Abs(stream.Stream.Bitrate - Bitrate) < 0.05f;
if (codecSame && channelsSame && bitrateSame)
{
args.Logger.ILog($"Stream {stream} matches codec, channels, and bitrate, skipping conversion");
return false;
}
stream.Codec = Codec.ToLowerInvariant();
stream.EncodingParameters.AddRange(FfmpegBuilderAudioAddTrack.GetNewAudioTrackParameters(args, stream, codec, channels, bitrate, 0));
return true;
}
}