Merge branch 'master' into pr/255

Former-commit-id: 2b7cd6f7ca
This commit is contained in:
Malte Rosenbjerg 2021-10-21 20:40:25 +02:00
commit 3ea68c4fb8
8 changed files with 348 additions and 6 deletions

View file

@ -414,5 +414,60 @@ public void Builder_BuildString_ForcePixelFormat()
.OutputToFile("output.mp4", false, opt => opt.ForcePixelFormat("yuv444p")).Arguments; .OutputToFile("output.mp4", false, opt => opt.ForcePixelFormat("yuv444p")).Arguments;
Assert.AreEqual("-i \"input.mp4\" -pix_fmt yuv444p \"output.mp4\"", str); Assert.AreEqual("-i \"input.mp4\" -pix_fmt yuv444p \"output.mp4\"", str);
} }
[TestMethod]
public void Builder_BuildString_PanAudioFilterChannelNumber()
{
var str = FFMpegArguments.FromFileInput("input.mp4")
.OutputToFile("output.mp4", false,
opt => opt.WithAudioFilters(filterOptions => filterOptions.Pan(2, "c0=c1", "c1=c1")))
.Arguments;
Assert.AreEqual("-i \"input.mp4\" -af \"pan=2c|c0=c1|c1=c1\" \"output.mp4\"", str);
}
[TestMethod]
public void Builder_BuildString_PanAudioFilterChannelLayout()
{
var str = FFMpegArguments.FromFileInput("input.mp4")
.OutputToFile("output.mp4", false,
opt => opt.WithAudioFilters(filterOptions => filterOptions.Pan("stereo", "c0=c0", "c1=c1")))
.Arguments;
Assert.AreEqual("-i \"input.mp4\" -af \"pan=stereo|c0=c0|c1=c1\" \"output.mp4\"", str);
}
[TestMethod]
public void Builder_BuildString_PanAudioFilterChannelNoOutputDefinition()
{
var str = FFMpegArguments.FromFileInput("input.mp4")
.OutputToFile("output.mp4", false,
opt => opt.WithAudioFilters(filterOptions => filterOptions.Pan("stereo")))
.Arguments;
Assert.AreEqual("-i \"input.mp4\" -af \"pan=stereo\" \"output.mp4\"", str);
}
[TestMethod]
public void Builder_BuildString_DynamicAudioNormalizerDefaultFormat()
{
var str = FFMpegArguments.FromFileInput("input.mp4")
.OutputToFile("output.mp4", false,
opt => opt.WithAudioFilters(filterOptions => filterOptions.DynamicNormalizer()))
.Arguments;
Assert.AreEqual("-i \"input.mp4\" -af \"dynaudnorm=f=500:g=31:p=0.95:m=10.0:r=0.0:n=1:c=0:b=0:s=0.0\" \"output.mp4\"", str);
}
[TestMethod]
public void Builder_BuildString_DynamicAudioNormalizerWithValuesFormat()
{
var str = FFMpegArguments.FromFileInput("input.mp4")
.OutputToFile("output.mp4", false,
opt => opt.WithAudioFilters(filterOptions => filterOptions.DynamicNormalizer(125, 13, 0.9215, 5.124, 0.5458,false,true,true, 0.3333333)))
.Arguments;
Assert.AreEqual("-i \"input.mp4\" -af \"dynaudnorm=f=125:g=13:p=0.92:m=5.1:r=0.5:n=0:c=1:b=1:s=0.3\" \"output.mp4\"", str);
}
} }
} }

View file

@ -1,5 +1,6 @@
using FFMpegCore.Enums; using FFMpegCore.Enums;
using FFMpegCore.Exceptions; using FFMpegCore.Exceptions;
using FFMpegCore.Extend;
using FFMpegCore.Pipes; using FFMpegCore.Pipes;
using FFMpegCore.Test.Resources; using FFMpegCore.Test.Resources;
using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.VisualStudio.TestTools.UnitTesting;
@ -8,7 +9,6 @@
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using FFMpegCore.Extend;
namespace FFMpegCore.Test namespace FFMpegCore.Test
{ {
@ -223,5 +223,111 @@ public void Audio_ToAAC_Args_Pipe_InvalidSampleRate()
.WithAudioCodec(AudioCodec.Aac)) .WithAudioCodec(AudioCodec.Aac))
.ProcessSynchronously()); .ProcessSynchronously());
} }
[TestMethod, Timeout(10000)]
public void Audio_Pan_ToMono()
{
using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
var success = FFMpegArguments.FromFileInput(TestResources.Mp3Audio)
.OutputToFile(outputFile, true,
argumentOptions => argumentOptions
.WithAudioFilters(filter => filter.Pan(1, "c0 < 0.9 * c0 + 0.1 * c1")))
.ProcessSynchronously();
var mediaAnalysis = FFProbe.Analyse(outputFile);
Assert.IsTrue(success);
Assert.AreEqual(1, mediaAnalysis.AudioStreams.Count);
Assert.AreEqual("mono", mediaAnalysis.PrimaryAudioStream.ChannelLayout);
}
[TestMethod, Timeout(10000)]
public void Audio_Pan_ToMonoNoDefinitions()
{
using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
var success = FFMpegArguments.FromFileInput(TestResources.Mp3Audio)
.OutputToFile(outputFile, true,
argumentOptions => argumentOptions
.WithAudioFilters(filter => filter.Pan(1)))
.ProcessSynchronously();
var mediaAnalysis = FFProbe.Analyse(outputFile);
Assert.IsTrue(success);
Assert.AreEqual(1, mediaAnalysis.AudioStreams.Count);
Assert.AreEqual("mono", mediaAnalysis.PrimaryAudioStream.ChannelLayout);
}
[TestMethod, Timeout(10000)]
public void Audio_Pan_ToMonoChannelsToOutputDefinitionsMismatch()
{
using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
var ex = Assert.ThrowsException<ArgumentException>(() => FFMpegArguments.FromFileInput(TestResources.Mp3Audio)
.OutputToFile(outputFile, true,
argumentOptions => argumentOptions
.WithAudioFilters(filter => filter.Pan(1, "c0=c0", "c1=c1")))
.ProcessSynchronously());
}
[TestMethod, Timeout(10000)]
public void Audio_Pan_ToMonoChannelsLayoutToOutputDefinitionsMismatch()
{
using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
var ex = Assert.ThrowsException<FFMpegException>(() => FFMpegArguments.FromFileInput(TestResources.Mp3Audio)
.OutputToFile(outputFile, true,
argumentOptions => argumentOptions
.WithAudioFilters(filter => filter.Pan("mono", "c0=c0", "c1=c1")))
.ProcessSynchronously());
}
[TestMethod, Timeout(10000)]
public void Audio_DynamicNormalizer_WithDefaultValues()
{
using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
var success = FFMpegArguments.FromFileInput(TestResources.Mp3Audio)
.OutputToFile(outputFile, true,
argumentOptions => argumentOptions
.WithAudioFilters(filter => filter.DynamicNormalizer()))
.ProcessSynchronously();
Assert.IsTrue(success);
}
[TestMethod, Timeout(10000)]
public void Audio_DynamicNormalizer_WithNonDefaultValues()
{
using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
var success = FFMpegArguments.FromFileInput(TestResources.Mp3Audio)
.OutputToFile(outputFile, true,
argumentOptions => argumentOptions
.WithAudioFilters(
filter => filter.DynamicNormalizer(250, 7, 0.9, 2, 1, false, true, true, 0.5)))
.ProcessSynchronously();
Assert.IsTrue(success);
}
[DataTestMethod, Timeout(10000)]
[DataRow(2)]
[DataRow(32)]
[DataRow(8)]
public void Audio_DynamicNormalizer_FilterWindow(int filterWindow)
{
using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
var ex = Assert.ThrowsException<ArgumentOutOfRangeException>(() => FFMpegArguments
.FromFileInput(TestResources.Mp3Audio)
.OutputToFile(outputFile, true,
argumentOptions => argumentOptions
.WithAudioFilters(
filter => filter.DynamicNormalizer(filterWindow: filterWindow)))
.ProcessSynchronously());
}
} }
} }

View file

@ -40,9 +40,9 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="GitHubActionsTestLogger" Version="1.2.0" /> <PackageReference Include="GitHubActionsTestLogger" Version="1.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.1" /> <PackageReference Include="MSTest.TestAdapter" Version="2.2.7" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.1" /> <PackageReference Include="MSTest.TestFramework" Version="2.2.7" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View file

@ -0,0 +1,60 @@
using System.Collections.Generic;
using System.Linq;
using FFMpegCore.Exceptions;
namespace FFMpegCore.Arguments
{
public class AudioFiltersArgument : IArgument
{
public readonly AudioFilterOptions Options;
public AudioFiltersArgument(AudioFilterOptions options)
{
Options = options;
}
public string Text => GetText();
private string GetText()
{
if (!Options.Arguments.Any())
throw new FFMpegArgumentException("No audio-filter arguments provided");
var arguments = Options.Arguments
.Where(arg => !string.IsNullOrEmpty(arg.Value))
.Select(arg =>
{
var escapedValue = arg.Value.Replace(",", "\\,");
return string.IsNullOrEmpty(arg.Key) ? escapedValue : $"{arg.Key}={escapedValue}";
});
return $"-af \"{string.Join(", ", arguments)}\"";
}
}
public interface IAudioFilterArgument
{
public string Key { get; }
public string Value { get; }
}
public class AudioFilterOptions
{
public List<IAudioFilterArgument> Arguments { get; } = new List<IAudioFilterArgument>();
public AudioFilterOptions Pan(string channelLayout, params string[] outputDefinitions) => WithArgument(new PanArgument(channelLayout, outputDefinitions));
public AudioFilterOptions Pan(int channels, params string[] outputDefinitions) => WithArgument(new PanArgument(channels, outputDefinitions));
public AudioFilterOptions DynamicNormalizer(int frameLength = 500, int filterWindow = 31, double targetPeak = 0.95,
double gainFactor = 10.0, double targetRms = 0.0, bool channelCoupling = true,
bool enableDcBiasCorrection = false, bool enableAlternativeBoundary = false,
double compressorFactor = 0.0) => WithArgument(new DynamicNormalizerArgument(frameLength, filterWindow,
targetPeak, gainFactor, targetRms, channelCoupling, enableDcBiasCorrection, enableAlternativeBoundary,
compressorFactor));
private AudioFilterOptions WithArgument(IAudioFilterArgument argument)
{
Arguments.Add(argument);
return this;
}
}
}

View file

@ -0,0 +1,49 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
namespace FFMpegCore.Arguments
{
public class DynamicNormalizerArgument : IAudioFilterArgument
{
private readonly Dictionary<string, string> _arguments = new Dictionary<string, string>();
/// <summary>
/// Dynamic Audio Normalizer. <see href="https://ffmpeg.org/ffmpeg-filters.html#dynaudnorm"/>
/// </summary>
/// <param name="frameLength">Set the frame length in milliseconds. Must be between 10 to 8000. The default value is 500</param>
/// <param name="filterWindow">Set the Gaussian filter window size. In range from 3 to 301, must be odd number. The default value is 31</param>
/// <param name="targetPeak">Set the target peak value. The default value is 0.95</param>
/// <param name="gainFactor">Set the maximum gain factor. In range from 1.0 to 100.0. Default is 10.0.</param>
/// <param name="targetRms">Set the target RMS. In range from 0.0 to 1.0. Default to 0.0 (disabled)</param>
/// <param name="channelCoupling">Enable channels coupling. By default is enabled.</param>
/// <param name="enableDcBiasCorrection">Enable DC bias correction. By default is disabled.</param>
/// <param name="enableAlternativeBoundary">Enable alternative boundary mode. By default is disabled.</param>
/// <param name="compressorFactor">Set the compress factor. In range from 0.0 to 30.0. Default is 0.0 (disabled).</param>
public DynamicNormalizerArgument(int frameLength = 500, int filterWindow = 31, double targetPeak = 0.95, double gainFactor = 10.0, double targetRms = 0.0, bool channelCoupling = true, bool enableDcBiasCorrection = false, bool enableAlternativeBoundary = false, double compressorFactor = 0.0)
{
if (frameLength < 10 || frameLength > 8000) throw new ArgumentOutOfRangeException(nameof(frameLength),"Frame length must be between 10 to 8000");
if (filterWindow < 3 || filterWindow > 31) throw new ArgumentOutOfRangeException(nameof(filterWindow), "Gaussian filter window size must be between 3 to 31");
if (filterWindow % 2 == 0) throw new ArgumentOutOfRangeException(nameof(filterWindow), "Gaussian filter window size must be an odd number");
if (targetPeak <= 0 || targetPeak > 1) throw new ArgumentOutOfRangeException(nameof(targetPeak));
if (gainFactor < 1 || gainFactor > 100) throw new ArgumentOutOfRangeException(nameof(gainFactor), "Gain factor must be between 1.0 to 100.0");
if (targetRms < 0 || targetRms > 1) throw new ArgumentOutOfRangeException(nameof(targetRms), "Target RMS must be between 0.0 and 1.0");
if (compressorFactor < 0 || compressorFactor > 30) throw new ArgumentOutOfRangeException(nameof(compressorFactor), "Compressor factor must be between 0.0 and 30.0");
_arguments.Add("f", frameLength.ToString());
_arguments.Add("g", filterWindow.ToString());
_arguments.Add("p", targetPeak.ToString("0.00", CultureInfo.InvariantCulture));
_arguments.Add("m", gainFactor.ToString("0.0", CultureInfo.InvariantCulture));
_arguments.Add("r", targetRms.ToString("0.0", CultureInfo.InvariantCulture));
_arguments.Add("n", (channelCoupling ? 1 : 0).ToString());
_arguments.Add("c", (enableDcBiasCorrection ? 1 : 0).ToString());
_arguments.Add("b", (enableAlternativeBoundary ? 1 : 0).ToString());
_arguments.Add("s", compressorFactor.ToString("0.0", CultureInfo.InvariantCulture));
}
public string Key { get; } = "dynaudnorm";
public string Value => string.Join(":", _arguments.Select(pair => $"{pair.Key}={pair.Value}"));
}
}

View file

@ -0,0 +1,59 @@
using System;
using System.Linq;
namespace FFMpegCore.Arguments
{
/// <summary>
/// Mix channels with specific gain levels.
/// </summary>
public class PanArgument : IAudioFilterArgument
{
public readonly string ChannelLayout;
private readonly string[] _outputDefinitions;
/// <summary>
/// Mix channels with specific gain levels <see href="https://ffmpeg.org/ffmpeg-filters.html#toc-pan-1"/>
/// </summary>
/// <param name="channelLayout">
/// Represent the output channel layout. Like "stereo", "mono", "2.1", "5.1"
/// </param>
/// <param name="outputDefinitions">
/// Output channel specification, of the form: "out_name=[gain*]in_name[(+-)[gain*]in_name...]"
/// </param>
public PanArgument(string channelLayout, params string[] outputDefinitions)
{
if (string.IsNullOrWhiteSpace(channelLayout))
{
throw new ArgumentException("The channel layout must be set" ,nameof(channelLayout));
}
ChannelLayout = channelLayout;
_outputDefinitions = outputDefinitions;
}
/// <summary>
/// Mix channels with specific gain levels <see href="https://ffmpeg.org/ffmpeg-filters.html#toc-pan-1"/>
/// </summary>
/// <param name="channels">Number of channels in output file</param>
/// <param name="outputDefinitions">
/// Output channel specification, of the form: "out_name=[gain*]in_name[(+-)[gain*]in_name...]"
/// </param>
public PanArgument(int channels, params string[] outputDefinitions)
{
if (channels <= 0) throw new ArgumentOutOfRangeException(nameof(channels));
if (outputDefinitions.Length > channels)
throw new ArgumentException("The number of output definitions must be equal or lower than number of channels", nameof(outputDefinitions));
ChannelLayout = $"{channels}c";
_outputDefinitions = outputDefinitions;
}
public string Key { get; } = "pan";
public string Value =>
string.Join("|", Enumerable.Empty<string>().Append(ChannelLayout).Concat(_outputDefinitions));
}
}

View file

@ -43,6 +43,13 @@ public FFMpegArgumentOptions WithVideoFilters(Action<VideoFilterOptions> videoFi
return WithArgument(new VideoFiltersArgument(videoFilterOptionsObj)); return WithArgument(new VideoFiltersArgument(videoFilterOptionsObj));
} }
public FFMpegArgumentOptions WithAudioFilters(Action<AudioFilterOptions> audioFilterOptions)
{
var audioFilterOptionsObj = new AudioFilterOptions();
audioFilterOptions(audioFilterOptionsObj);
return WithArgument(new AudioFiltersArgument(audioFilterOptionsObj));
}
public FFMpegArgumentOptions WithFramerate(double framerate) => WithArgument(new FrameRateArgument(framerate)); public FFMpegArgumentOptions WithFramerate(double framerate) => WithArgument(new FrameRateArgument(framerate));
public FFMpegArgumentOptions WithoutMetadata() => WithArgument(new RemoveMetadataArgument()); public FFMpegArgumentOptions WithoutMetadata() => WithArgument(new RemoveMetadataArgument());
public FFMpegArgumentOptions WithSpeedPreset(Speed speed) => WithArgument(new SpeedPresetArgument(speed)); public FFMpegArgumentOptions WithSpeedPreset(Speed speed) => WithArgument(new SpeedPresetArgument(speed));

View file

@ -75,7 +75,10 @@ public static async Task<IMediaAnalysis> AnalyseAsync(string filePath, int outpu
throw new FFMpegException(FFMpegExceptionType.File, $"No file found at '{filePath}'"); throw new FFMpegException(FFMpegExceptionType.File, $"No file found at '{filePath}'");
using var instance = PrepareStreamAnalysisInstance(filePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current); using var instance = PrepareStreamAnalysisInstance(filePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current);
await instance.FinishedRunning().ConfigureAwait(false); var exitCode = await instance.FinishedRunning().ConfigureAwait(false);
if (exitCode != 0)
throw new FFMpegException(FFMpegExceptionType.Process, $"ffprobe exited with non-zero exit-code ({exitCode} - {string.Join("\n", instance.ErrorData)})", null, string.Join("\n", instance.ErrorData));
return ParseOutput(instance); return ParseOutput(instance);
} }
@ -91,7 +94,10 @@ public static async Task<FFProbeFrames> GetFramesAsync(string filePath, int outp
public static async Task<IMediaAnalysis> AnalyseAsync(Uri uri, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null) public static async Task<IMediaAnalysis> AnalyseAsync(Uri uri, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null)
{ {
using var instance = PrepareStreamAnalysisInstance(uri.AbsoluteUri, outputCapacity, ffOptions ?? GlobalFFOptions.Current); using var instance = PrepareStreamAnalysisInstance(uri.AbsoluteUri, outputCapacity, ffOptions ?? GlobalFFOptions.Current);
await instance.FinishedRunning().ConfigureAwait(false); var exitCode = await instance.FinishedRunning().ConfigureAwait(false);
if (exitCode != 0)
throw new FFMpegException(FFMpegExceptionType.Process, $"ffprobe exited with non-zero exit-code ({exitCode} - {string.Join("\n", instance.ErrorData)})", null, string.Join("\n", instance.ErrorData));
return ParseOutput(instance); return ParseOutput(instance);
} }
public static async Task<IMediaAnalysis> AnalyseAsync(Stream stream, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null) public static async Task<IMediaAnalysis> AnalyseAsync(Stream stream, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null)