mirror of
https://github.com/rosenbjerg/FFMpegCore.git
synced 2024-11-10 08:34:12 +01:00
Merge pull request #261 from alex6dj/feature/audio-filters
Audio filter implementation
This commit is contained in:
commit
3d701b138c
7 changed files with 340 additions and 4 deletions
|
@ -414,5 +414,60 @@ public void Builder_BuildString_ForcePixelFormat()
|
|||
.OutputToFile("output.mp4", false, opt => opt.ForcePixelFormat("yuv444p")).Arguments;
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
using FFMpegCore.Enums;
|
||||
using FFMpegCore.Exceptions;
|
||||
using FFMpegCore.Extend;
|
||||
using FFMpegCore.Pipes;
|
||||
using FFMpegCore.Test.Resources;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
@ -8,7 +9,6 @@
|
|||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using FFMpegCore.Extend;
|
||||
|
||||
namespace FFMpegCore.Test
|
||||
{
|
||||
|
@ -223,5 +223,111 @@ public void Audio_ToAAC_Args_Pipe_InvalidSampleRate()
|
|||
.WithAudioCodec(AudioCodec.Aac))
|
||||
.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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -40,9 +40,9 @@
|
|||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="GitHubActionsTestLogger" Version="1.2.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="2.2.1" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="2.2.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="2.2.7" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="2.2.7" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
60
FFMpegCore/FFMpeg/Arguments/AudioFiltersArgument.cs
Normal file
60
FFMpegCore/FFMpeg/Arguments/AudioFiltersArgument.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
49
FFMpegCore/FFMpeg/Arguments/DynamicNormalizerArgument.cs
Normal file
49
FFMpegCore/FFMpeg/Arguments/DynamicNormalizerArgument.cs
Normal 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}"));
|
||||
}
|
||||
}
|
59
FFMpegCore/FFMpeg/Arguments/PanArgument.cs
Normal file
59
FFMpegCore/FFMpeg/Arguments/PanArgument.cs
Normal 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));
|
||||
}
|
||||
}
|
|
@ -43,6 +43,13 @@ public FFMpegArgumentOptions WithVideoFilters(Action<VideoFilterOptions> videoFi
|
|||
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 WithoutMetadata() => WithArgument(new RemoveMetadataArgument());
|
||||
public FFMpegArgumentOptions WithSpeedPreset(Speed speed) => WithArgument(new SpeedPresetArgument(speed));
|
||||
|
|
Loading…
Reference in a new issue