add loudness normalization (#129)
* fix music video search result artwork * add normalize loudness setting * fix audio normalization * fix music video thumbnails in collection items view * fix ef core warnings querying playout item * implement audio loudness normalization filter
This commit is contained in:
@@ -18,7 +18,7 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
|||||||
string AudioCodec,
|
string AudioCodec,
|
||||||
int AudioBitrate,
|
int AudioBitrate,
|
||||||
int AudioBufferSize,
|
int AudioBufferSize,
|
||||||
int AudioVolume,
|
bool NormalizeLoudness,
|
||||||
int AudioChannels,
|
int AudioChannels,
|
||||||
int AudioSampleRate,
|
int AudioSampleRate,
|
||||||
bool NormalizeAudio,
|
bool NormalizeAudio,
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
|||||||
AudioCodec = request.AudioCodec,
|
AudioCodec = request.AudioCodec,
|
||||||
AudioBitrate = request.AudioBitrate,
|
AudioBitrate = request.AudioBitrate,
|
||||||
AudioBufferSize = request.AudioBufferSize,
|
AudioBufferSize = request.AudioBufferSize,
|
||||||
AudioVolume = request.AudioVolume,
|
NormalizeLoudness= request.NormalizeLoudness,
|
||||||
AudioChannels = request.AudioChannels,
|
AudioChannels = request.AudioChannels,
|
||||||
AudioSampleRate = request.AudioSampleRate,
|
AudioSampleRate = request.AudioSampleRate,
|
||||||
NormalizeAudio = request.NormalizeAudio,
|
NormalizeAudio = request.NormalizeAudio,
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
|||||||
string AudioCodec,
|
string AudioCodec,
|
||||||
int AudioBitrate,
|
int AudioBitrate,
|
||||||
int AudioBufferSize,
|
int AudioBufferSize,
|
||||||
int AudioVolume,
|
bool NormalizeLoudness,
|
||||||
int AudioChannels,
|
int AudioChannels,
|
||||||
int AudioSampleRate,
|
int AudioSampleRate,
|
||||||
bool NormalizeAudio,
|
bool NormalizeAudio,
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
|||||||
p.AudioCodec = update.AudioCodec;
|
p.AudioCodec = update.AudioCodec;
|
||||||
p.AudioBitrate = update.AudioBitrate;
|
p.AudioBitrate = update.AudioBitrate;
|
||||||
p.AudioBufferSize = update.AudioBufferSize;
|
p.AudioBufferSize = update.AudioBufferSize;
|
||||||
p.AudioVolume = update.AudioVolume;
|
p.NormalizeLoudness = update.NormalizeLoudness;
|
||||||
p.AudioChannels = update.AudioChannels;
|
p.AudioChannels = update.AudioChannels;
|
||||||
p.AudioSampleRate = update.AudioSampleRate;
|
p.AudioSampleRate = update.AudioSampleRate;
|
||||||
p.NormalizeAudio = update.NormalizeAudio;
|
p.NormalizeAudio = update.NormalizeAudio;
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ namespace ErsatzTV.Application.FFmpegProfiles
|
|||||||
string AudioCodec,
|
string AudioCodec,
|
||||||
int AudioBitrate,
|
int AudioBitrate,
|
||||||
int AudioBufferSize,
|
int AudioBufferSize,
|
||||||
int AudioVolume,
|
bool NormalizeLoudness,
|
||||||
int AudioChannels,
|
int AudioChannels,
|
||||||
int AudioSampleRate,
|
int AudioSampleRate,
|
||||||
bool NormalizeAudio,
|
bool NormalizeAudio,
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ namespace ErsatzTV.Application.FFmpegProfiles
|
|||||||
profile.AudioCodec,
|
profile.AudioCodec,
|
||||||
profile.AudioBitrate,
|
profile.AudioBitrate,
|
||||||
profile.AudioBufferSize,
|
profile.AudioBufferSize,
|
||||||
profile.AudioVolume,
|
profile.NormalizeLoudness,
|
||||||
profile.AudioChannels,
|
profile.AudioChannels,
|
||||||
profile.AudioSampleRate,
|
profile.AudioSampleRate,
|
||||||
profile.NormalizeAudio,
|
profile.NormalizeAudio,
|
||||||
|
|||||||
+87
-15
@@ -581,7 +581,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void Should_SetCopyAudioCodec_When_CorrectCodec_ForTransportStream()
|
public void Should_SetDesiredAudioCodec_When_NormalizingAudio_With_CorrectCodec_ForTransportStream()
|
||||||
{
|
{
|
||||||
FFmpegProfile ffmpegProfile = TestProfile() with
|
FFmpegProfile ffmpegProfile = TestProfile() with
|
||||||
{
|
{
|
||||||
@@ -600,11 +600,11 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
DateTimeOffset.Now,
|
DateTimeOffset.Now,
|
||||||
DateTimeOffset.Now);
|
DateTimeOffset.Now);
|
||||||
|
|
||||||
actual.AudioCodec.Should().Be("copy");
|
actual.AudioCodec.Should().Be("aac");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void Should_SetCopyAudioCodec_When_NotNormalizingVideo_ForTransportStream()
|
public void Should_SetCopyAudioCodec_When_NotNormalizingAudio_ForTransportStream()
|
||||||
{
|
{
|
||||||
FFmpegProfile ffmpegProfile = TestProfile() with
|
FFmpegProfile ffmpegProfile = TestProfile() with
|
||||||
{
|
{
|
||||||
@@ -627,7 +627,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void Should_SetDesiredAudioCodec_When_NormalizingVideo_ForTransportStream()
|
public void Should_SetDesiredAudioCodec_When_NormalizingAudio_ForTransportStream()
|
||||||
{
|
{
|
||||||
FFmpegProfile ffmpegProfile = TestProfile() with
|
FFmpegProfile ffmpegProfile = TestProfile() with
|
||||||
{
|
{
|
||||||
@@ -650,7 +650,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void Should_SetCopyAudioCodec_When_NormalizingVideo_ForHttpLiveStreaming()
|
public void Should_SetCopyAudioCodec_When_NormalizingAudio_ForHttpLiveStreaming()
|
||||||
{
|
{
|
||||||
FFmpegProfile ffmpegProfile = TestProfile() with
|
FFmpegProfile ffmpegProfile = TestProfile() with
|
||||||
{
|
{
|
||||||
@@ -673,12 +673,13 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void Should_SetAudioBitrate_When_NormalizingVideo_ForTransportStream()
|
public void Should_SetAudioBitrate_When_NormalizingAudio_With_CorrectCodec_ForTransportStream()
|
||||||
{
|
{
|
||||||
FFmpegProfile ffmpegProfile = TestProfile() with
|
FFmpegProfile ffmpegProfile = TestProfile() with
|
||||||
{
|
{
|
||||||
NormalizeAudio = true,
|
NormalizeAudio = true,
|
||||||
AudioBitrate = 2424
|
AudioBitrate = 2424,
|
||||||
|
AudioCodec = "ac3"
|
||||||
};
|
};
|
||||||
|
|
||||||
var version = new MediaVersion();
|
var version = new MediaVersion();
|
||||||
@@ -696,12 +697,13 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void Should_SetAudioBufferSize_When_NormalizingVideo_ForTransportStream()
|
public void Should_SetAudioBufferSize_When_NormalizingAudio_With_CorrectCodec_ForTransportStream()
|
||||||
{
|
{
|
||||||
FFmpegProfile ffmpegProfile = TestProfile() with
|
FFmpegProfile ffmpegProfile = TestProfile() with
|
||||||
{
|
{
|
||||||
NormalizeAudio = true,
|
NormalizeAudio = true,
|
||||||
AudioBufferSize = 2424
|
AudioBufferSize = 2424,
|
||||||
|
AudioCodec = "ac3"
|
||||||
};
|
};
|
||||||
|
|
||||||
var version = new MediaVersion();
|
var version = new MediaVersion();
|
||||||
@@ -719,7 +721,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void ShouldNot_SetAudioChannels_When_CorrectCodec_ForTransportStream()
|
public void Should_SetAudioChannels_When_NormalizingAudio_With_CorrectCodec_ForTransportStream()
|
||||||
{
|
{
|
||||||
FFmpegProfile ffmpegProfile = TestProfile() with
|
FFmpegProfile ffmpegProfile = TestProfile() with
|
||||||
{
|
{
|
||||||
@@ -739,11 +741,11 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
DateTimeOffset.Now,
|
DateTimeOffset.Now,
|
||||||
DateTimeOffset.Now);
|
DateTimeOffset.Now);
|
||||||
|
|
||||||
actual.AudioChannels.IsNone.Should().BeTrue();
|
actual.AudioChannels.IfNone(0).Should().Be(6);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void ShouldNot_SetAudioSampleRate_When_CorrectCodec_ForTransportStream()
|
public void Should_SetAudioSampleRate_When_NormalizingAudio_With_CorrectCodec_ForTransportStream()
|
||||||
{
|
{
|
||||||
FFmpegProfile ffmpegProfile = TestProfile() with
|
FFmpegProfile ffmpegProfile = TestProfile() with
|
||||||
{
|
{
|
||||||
@@ -763,11 +765,11 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
DateTimeOffset.Now,
|
DateTimeOffset.Now,
|
||||||
DateTimeOffset.Now);
|
DateTimeOffset.Now);
|
||||||
|
|
||||||
actual.AudioSampleRate.IsNone.Should().BeTrue();
|
actual.AudioSampleRate.IfNone(0).Should().Be(48);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void Should_SetAudioChannels_When_NormalizingVideoAndAudio_ForTransportStream()
|
public void Should_SetAudioChannels_When_NormalizingAudio_ForTransportStream()
|
||||||
{
|
{
|
||||||
FFmpegProfile ffmpegProfile = TestProfile() with
|
FFmpegProfile ffmpegProfile = TestProfile() with
|
||||||
{
|
{
|
||||||
@@ -790,7 +792,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void Should_SetAudioSampleRate_When_NormalizingVideoAndAudio_ForTransportStream()
|
public void Should_SetAudioSampleRate_When_NormalizingAudio_ForTransportStream()
|
||||||
{
|
{
|
||||||
FFmpegProfile ffmpegProfile = TestProfile() with
|
FFmpegProfile ffmpegProfile = TestProfile() with
|
||||||
{
|
{
|
||||||
@@ -811,6 +813,76 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
|
|
||||||
actual.AudioSampleRate.IfNone(0).Should().Be(48);
|
actual.AudioSampleRate.IfNone(0).Should().Be(48);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Should_SetAudioDuration_When_NormalizingAudio_With_CorrectCodec_ForTransportStream()
|
||||||
|
{
|
||||||
|
FFmpegProfile ffmpegProfile = TestProfile() with
|
||||||
|
{
|
||||||
|
NormalizeAudio = true,
|
||||||
|
AudioSampleRate = 48,
|
||||||
|
AudioCodec = "ac3"
|
||||||
|
};
|
||||||
|
|
||||||
|
var version = new MediaVersion { Duration = TimeSpan.FromMinutes(2) };
|
||||||
|
|
||||||
|
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
|
||||||
|
StreamingMode.TransportStream,
|
||||||
|
ffmpegProfile,
|
||||||
|
version,
|
||||||
|
new MediaStream(),
|
||||||
|
new MediaStream { Codec = "ac3" },
|
||||||
|
DateTimeOffset.Now,
|
||||||
|
DateTimeOffset.Now);
|
||||||
|
|
||||||
|
actual.AudioDuration.IfNone(TimeSpan.MinValue).Should().Be(TimeSpan.FromMinutes(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Should_SetNormalizeLoudness_When_NormalizingAudio_ForTransportStream()
|
||||||
|
{
|
||||||
|
FFmpegProfile ffmpegProfile = TestProfile() with
|
||||||
|
{
|
||||||
|
NormalizeAudio = true,
|
||||||
|
NormalizeLoudness = true
|
||||||
|
};
|
||||||
|
|
||||||
|
var version = new MediaVersion();
|
||||||
|
|
||||||
|
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
|
||||||
|
StreamingMode.TransportStream,
|
||||||
|
ffmpegProfile,
|
||||||
|
version,
|
||||||
|
new MediaStream(),
|
||||||
|
new MediaStream { Codec = "ac3" },
|
||||||
|
DateTimeOffset.Now,
|
||||||
|
DateTimeOffset.Now);
|
||||||
|
|
||||||
|
actual.NormalizeLoudness.Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Should_NotSetNormalizeLoudness_When_NotNormalizingAudio_ForTransportStream()
|
||||||
|
{
|
||||||
|
FFmpegProfile ffmpegProfile = TestProfile() with
|
||||||
|
{
|
||||||
|
NormalizeAudio = false,
|
||||||
|
NormalizeLoudness = true
|
||||||
|
};
|
||||||
|
|
||||||
|
var version = new MediaVersion();
|
||||||
|
|
||||||
|
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
|
||||||
|
StreamingMode.TransportStream,
|
||||||
|
ffmpegProfile,
|
||||||
|
version,
|
||||||
|
new MediaStream(),
|
||||||
|
new MediaStream { Codec = "ac3" },
|
||||||
|
DateTimeOffset.Now,
|
||||||
|
DateTimeOffset.Now);
|
||||||
|
|
||||||
|
actual.NormalizeLoudness.Should().BeFalse();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestFixture]
|
[TestFixture]
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
public string AudioCodec { get; set; }
|
public string AudioCodec { get; set; }
|
||||||
public int AudioBitrate { get; set; }
|
public int AudioBitrate { get; set; }
|
||||||
public int AudioBufferSize { get; set; }
|
public int AudioBufferSize { get; set; }
|
||||||
public int AudioVolume { get; set; }
|
public bool NormalizeLoudness { get; set; }
|
||||||
public int AudioChannels { get; set; }
|
public int AudioChannels { get; set; }
|
||||||
public int AudioSampleRate { get; set; }
|
public int AudioSampleRate { get; set; }
|
||||||
public bool NormalizeAudio { get; set; }
|
public bool NormalizeAudio { get; set; }
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
VideoBufferSize = 4000,
|
VideoBufferSize = 4000,
|
||||||
AudioBitrate = 192,
|
AudioBitrate = 192,
|
||||||
AudioBufferSize = 384,
|
AudioBufferSize = 384,
|
||||||
AudioVolume = 100,
|
NormalizeLoudness = true,
|
||||||
AudioChannels = 2,
|
AudioChannels = 2,
|
||||||
AudioSampleRate = 48,
|
AudioSampleRate = 48,
|
||||||
NormalizeVideo = true,
|
NormalizeVideo = true,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
private Option<string> _frameRate = None;
|
private Option<string> _frameRate = None;
|
||||||
private Option<HardwareAccelerationKind> _hardwareAccelerationKind = None;
|
private Option<HardwareAccelerationKind> _hardwareAccelerationKind = None;
|
||||||
private string _inputCodec;
|
private string _inputCodec;
|
||||||
|
private bool _normalizeLoudness;
|
||||||
private Option<IDisplaySize> _padToSize = None;
|
private Option<IDisplaySize> _padToSize = None;
|
||||||
private Option<IDisplaySize> _scaleToSize = None;
|
private Option<IDisplaySize> _scaleToSize = None;
|
||||||
|
|
||||||
@@ -49,6 +50,12 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public FFmpegComplexFilterBuilder WithNormalizeLoudness(bool normalizeLoudness)
|
||||||
|
{
|
||||||
|
_normalizeLoudness = normalizeLoudness;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public FFmpegComplexFilterBuilder WithInputCodec(string codec)
|
public FFmpegComplexFilterBuilder WithInputCodec(string codec)
|
||||||
{
|
{
|
||||||
_inputCodec = codec;
|
_inputCodec = codec;
|
||||||
@@ -77,22 +84,22 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
_ => false
|
_ => false
|
||||||
};
|
};
|
||||||
|
|
||||||
_audioDuration.IfSome(
|
var audioFilterQueue = new List<string>();
|
||||||
audioDuration =>
|
var videoFilterQueue = new List<string>();
|
||||||
{
|
|
||||||
complexFilter.Append($"[{audioLabel}]");
|
|
||||||
complexFilter.Append($"apad=whole_dur={audioDuration.TotalMilliseconds}ms");
|
|
||||||
audioLabel = "[a]";
|
|
||||||
complexFilter.Append(audioLabel);
|
|
||||||
});
|
|
||||||
|
|
||||||
var filterQueue = new List<string>();
|
if (_normalizeLoudness)
|
||||||
|
{
|
||||||
|
audioFilterQueue.Add("loudnorm=I=-16:TP=-1.5:LRA=11");
|
||||||
|
}
|
||||||
|
|
||||||
|
_audioDuration.IfSome(
|
||||||
|
audioDuration => audioFilterQueue.Add($"apad=whole_dur={audioDuration.TotalMilliseconds}ms"));
|
||||||
|
|
||||||
bool usesHardwareFilters = acceleration != HardwareAccelerationKind.None && !isHardwareDecode &&
|
bool usesHardwareFilters = acceleration != HardwareAccelerationKind.None && !isHardwareDecode &&
|
||||||
(_deinterlace || _scaleToSize.IsSome);
|
(_deinterlace || _scaleToSize.IsSome);
|
||||||
if (usesHardwareFilters)
|
if (usesHardwareFilters)
|
||||||
{
|
{
|
||||||
filterQueue.Add("hwupload");
|
videoFilterQueue.Add("hwupload");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_deinterlace)
|
if (_deinterlace)
|
||||||
@@ -107,11 +114,11 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(filter))
|
if (!string.IsNullOrWhiteSpace(filter))
|
||||||
{
|
{
|
||||||
filterQueue.Add(filter);
|
videoFilterQueue.Add(filter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_frameRate.IfSome(frameRate => filterQueue.Add($"fps=fps={frameRate}"));
|
_frameRate.IfSome(frameRate => videoFilterQueue.Add($"fps=fps={frameRate}"));
|
||||||
|
|
||||||
_scaleToSize.IfSome(
|
_scaleToSize.IfSome(
|
||||||
size =>
|
size =>
|
||||||
@@ -126,7 +133,7 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(filter))
|
if (!string.IsNullOrWhiteSpace(filter))
|
||||||
{
|
{
|
||||||
filterQueue.Add(filter);
|
videoFilterQueue.Add(filter);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -134,19 +141,19 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
{
|
{
|
||||||
if (acceleration != HardwareAccelerationKind.None && (isHardwareDecode || usesHardwareFilters))
|
if (acceleration != HardwareAccelerationKind.None && (isHardwareDecode || usesHardwareFilters))
|
||||||
{
|
{
|
||||||
filterQueue.Add("hwdownload");
|
videoFilterQueue.Add("hwdownload");
|
||||||
string format = acceleration switch
|
string format = acceleration switch
|
||||||
{
|
{
|
||||||
HardwareAccelerationKind.Vaapi => "format=nv12|vaapi",
|
HardwareAccelerationKind.Vaapi => "format=nv12|vaapi",
|
||||||
_ => "format=nv12"
|
_ => "format=nv12"
|
||||||
};
|
};
|
||||||
filterQueue.Add(format);
|
videoFilterQueue.Add(format);
|
||||||
}
|
}
|
||||||
|
|
||||||
filterQueue.Add("setsar=1");
|
videoFilterQueue.Add("setsar=1");
|
||||||
}
|
}
|
||||||
|
|
||||||
_padToSize.IfSome(size => filterQueue.Add($"pad={size.Width}:{size.Height}:(ow-iw)/2:(oh-ih)/2"));
|
_padToSize.IfSome(size => videoFilterQueue.Add($"pad={size.Width}:{size.Height}:(ow-iw)/2:(oh-ih)/2"));
|
||||||
|
|
||||||
if ((_scaleToSize.IsSome || _padToSize.IsSome) && acceleration != HardwareAccelerationKind.None)
|
if ((_scaleToSize.IsSome || _padToSize.IsSome) && acceleration != HardwareAccelerationKind.None)
|
||||||
{
|
{
|
||||||
@@ -155,19 +162,27 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
HardwareAccelerationKind.Qsv => "hwupload=extra_hw_frames=64",
|
HardwareAccelerationKind.Qsv => "hwupload=extra_hw_frames=64",
|
||||||
_ => "hwupload"
|
_ => "hwupload"
|
||||||
};
|
};
|
||||||
filterQueue.Add(upload);
|
videoFilterQueue.Add(upload);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filterQueue.Any())
|
bool hasAudioFilters = audioFilterQueue.Any();
|
||||||
|
if (hasAudioFilters)
|
||||||
{
|
{
|
||||||
// TODO: any audio filter
|
complexFilter.Append($"[{audioLabel}]");
|
||||||
if (_audioDuration.IsSome)
|
complexFilter.Append(string.Join(",", audioFilterQueue));
|
||||||
|
audioLabel = "[a]";
|
||||||
|
complexFilter.Append(audioLabel);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (videoFilterQueue.Any())
|
||||||
|
{
|
||||||
|
if (hasAudioFilters)
|
||||||
{
|
{
|
||||||
complexFilter.Append(';');
|
complexFilter.Append(';');
|
||||||
}
|
}
|
||||||
|
|
||||||
complexFilter.Append($"[{videoLabel}]");
|
complexFilter.Append($"[{videoLabel}]");
|
||||||
complexFilter.Append(string.Join(",", filterQueue));
|
complexFilter.Append(string.Join(",", videoFilterQueue));
|
||||||
videoLabel = "[v]";
|
videoLabel = "[v]";
|
||||||
complexFilter.Append(videoLabel);
|
complexFilter.Append(videoLabel);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,5 +28,6 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
public bool Deinterlace { get; set; }
|
public bool Deinterlace { get; set; }
|
||||||
public Option<string> FrameRate { get; set; }
|
public Option<string> FrameRate { get; set; }
|
||||||
public Option<int> VideoTrackTimeScale { get; set; }
|
public Option<int> VideoTrackTimeScale { get; set; }
|
||||||
|
public bool NormalizeLoudness { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,22 +107,20 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
result.VideoCodec = "copy";
|
result.VideoCodec = "copy";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (NeedToNormalizeAudioCodec(ffmpegProfile, audioStream))
|
if (ffmpegProfile.NormalizeAudio)
|
||||||
{
|
{
|
||||||
result.AudioCodec = ffmpegProfile.AudioCodec;
|
result.AudioCodec = ffmpegProfile.AudioCodec;
|
||||||
result.AudioBitrate = ffmpegProfile.AudioBitrate;
|
result.AudioBitrate = ffmpegProfile.AudioBitrate;
|
||||||
result.AudioBufferSize = ffmpegProfile.AudioBufferSize;
|
result.AudioBufferSize = ffmpegProfile.AudioBufferSize;
|
||||||
|
|
||||||
if (ffmpegProfile.NormalizeAudio)
|
if (audioStream.Channels != ffmpegProfile.AudioChannels)
|
||||||
{
|
{
|
||||||
if (audioStream.Channels != ffmpegProfile.AudioChannels)
|
result.AudioChannels = ffmpegProfile.AudioChannels;
|
||||||
{
|
|
||||||
result.AudioChannels = ffmpegProfile.AudioChannels;
|
|
||||||
}
|
|
||||||
|
|
||||||
result.AudioSampleRate = ffmpegProfile.AudioSampleRate;
|
|
||||||
result.AudioDuration = version.Duration;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
result.AudioSampleRate = ffmpegProfile.AudioSampleRate;
|
||||||
|
result.AudioDuration = version.Duration;
|
||||||
|
result.NormalizeLoudness = ffmpegProfile.NormalizeLoudness;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -170,9 +168,6 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
private static bool NeedToNormalizeVideoCodec(FFmpegProfile ffmpegProfile, MediaStream videoStream) =>
|
private static bool NeedToNormalizeVideoCodec(FFmpegProfile ffmpegProfile, MediaStream videoStream) =>
|
||||||
ffmpegProfile.NormalizeVideo && ffmpegProfile.VideoCodec != videoStream.Codec;
|
ffmpegProfile.NormalizeVideo && ffmpegProfile.VideoCodec != videoStream.Codec;
|
||||||
|
|
||||||
private static bool NeedToNormalizeAudioCodec(FFmpegProfile ffmpegProfile, MediaStream audioStream) =>
|
|
||||||
ffmpegProfile.NormalizeAudio && ffmpegProfile.AudioCodec != audioStream.Codec;
|
|
||||||
|
|
||||||
private static IDisplaySize CalculateScaledSize(FFmpegProfile ffmpegProfile, MediaVersion version)
|
private static IDisplaySize CalculateScaledSize(FFmpegProfile ffmpegProfile, MediaVersion version)
|
||||||
{
|
{
|
||||||
IDisplaySize sarSize = SARSize(version);
|
IDisplaySize sarSize = SARSize(version);
|
||||||
|
|||||||
@@ -324,6 +324,12 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public FFmpegProcessBuilder WithNormalizeLoudness(bool normalizeLoudness)
|
||||||
|
{
|
||||||
|
_complexFilterBuilder = _complexFilterBuilder.WithNormalizeLoudness(normalizeLoudness);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public FFmpegProcessBuilder WithFrameRate(Option<string> frameRate)
|
public FFmpegProcessBuilder WithFrameRate(Option<string> frameRate)
|
||||||
{
|
{
|
||||||
_complexFilterBuilder = _complexFilterBuilder.WithFrameRate(frameRate);
|
_complexFilterBuilder = _complexFilterBuilder.WithFrameRate(frameRate);
|
||||||
|
|||||||
@@ -50,7 +50,9 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
.WithSeek(playbackSettings.StreamSeek)
|
.WithSeek(playbackSettings.StreamSeek)
|
||||||
.WithInputCodec(path, playbackSettings.HardwareAcceleration, videoStream.Codec)
|
.WithInputCodec(path, playbackSettings.HardwareAcceleration, videoStream.Codec)
|
||||||
.WithFrameRate(playbackSettings.FrameRate)
|
.WithFrameRate(playbackSettings.FrameRate)
|
||||||
.WithVideoTrackTimeScale(playbackSettings.VideoTrackTimeScale);
|
.WithVideoTrackTimeScale(playbackSettings.VideoTrackTimeScale)
|
||||||
|
.WithAlignedAudio(playbackSettings.AudioDuration)
|
||||||
|
.WithNormalizeLoudness(playbackSettings.NormalizeLoudness);
|
||||||
|
|
||||||
playbackSettings.ScaledSize.Match(
|
playbackSettings.ScaledSize.Match(
|
||||||
scaledSize =>
|
scaledSize =>
|
||||||
@@ -65,7 +67,6 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
}
|
}
|
||||||
|
|
||||||
builder = builder
|
builder = builder
|
||||||
.WithAlignedAudio(playbackSettings.AudioDuration)
|
|
||||||
.WithFilterComplex(videoStream.Index, audioStream.Index);
|
.WithFilterComplex(videoStream.Index, audioStream.Index);
|
||||||
},
|
},
|
||||||
() =>
|
() =>
|
||||||
@@ -75,7 +76,6 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
builder = builder
|
builder = builder
|
||||||
.WithDeinterlace(playbackSettings.Deinterlace)
|
.WithDeinterlace(playbackSettings.Deinterlace)
|
||||||
.WithBlackBars(channel.FFmpegProfile.Resolution)
|
.WithBlackBars(channel.FFmpegProfile.Resolution)
|
||||||
.WithAlignedAudio(playbackSettings.AudioDuration)
|
|
||||||
.WithFilterComplex(videoStream.Index, audioStream.Index);
|
.WithFilterComplex(videoStream.Index, audioStream.Index);
|
||||||
}
|
}
|
||||||
else if (playbackSettings.Deinterlace)
|
else if (playbackSettings.Deinterlace)
|
||||||
@@ -87,7 +87,6 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
builder = builder
|
builder = builder
|
||||||
.WithAlignedAudio(playbackSettings.AudioDuration)
|
|
||||||
.WithFilterComplex(videoStream.Index, audioStream.Index);
|
.WithFilterComplex(videoStream.Index, audioStream.Index);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
.ThenInclude(mi => (mi as MusicVideo).MediaVersions)
|
.ThenInclude(mi => (mi as MusicVideo).MediaVersions)
|
||||||
.ThenInclude(mv => mv.Streams)
|
.ThenInclude(mv => mv.Streams)
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
|
.OrderBy(pi => pi.Start)
|
||||||
.SingleOrDefaultAsync()
|
.SingleOrDefaultAsync()
|
||||||
.Map(Optional);
|
.Map(Optional);
|
||||||
|
|
||||||
|
|||||||
Generated
+1961
File diff suppressed because it is too large
Load Diff
+34
@@ -0,0 +1,34 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
public partial class Add_FFmpegProfile_NormalizeLoudness : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
"AudioVolume",
|
||||||
|
"FFmpegProfile");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
"NormalizeLoudness",
|
||||||
|
"FFmpegProfile",
|
||||||
|
"INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
"NormalizeLoudness",
|
||||||
|
"FFmpegProfile");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
"AudioVolume",
|
||||||
|
"FFmpegProfile",
|
||||||
|
"INTEGER",
|
||||||
|
nullable: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -184,6 +184,7 @@
|
|||||||
{
|
{
|
||||||
<MediaCard Data="@card"
|
<MediaCard Data="@card"
|
||||||
Link=""
|
Link=""
|
||||||
|
ArtworkKind="ArtworkKind.Thumbnail"
|
||||||
DeleteClicked="@RemoveMusicVideoFromCollection"
|
DeleteClicked="@RemoveMusicVideoFromCollection"
|
||||||
SelectColor="@Color.Error"
|
SelectColor="@Color.Error"
|
||||||
SelectClicked="@(e => SelectClicked(card, e))"
|
SelectClicked="@(e => SelectClicked(card, e))"
|
||||||
|
|||||||
@@ -61,32 +61,31 @@
|
|||||||
<MudElement HtmlTag="div" Class="mt-3">
|
<MudElement HtmlTag="div" Class="mt-3">
|
||||||
<MudTextField Disabled="@(!_model.Transcode)" Label="Frame Rate" @bind-Value="_model.FrameRate" For="@(() => _model.FrameRate)" Adornment="Adornment.End" AdornmentText="fps"/>
|
<MudTextField Disabled="@(!_model.Transcode)" Label="Frame Rate" @bind-Value="_model.FrameRate" For="@(() => _model.FrameRate)" Adornment="Adornment.End" AdornmentText="fps"/>
|
||||||
</MudElement>
|
</MudElement>
|
||||||
|
<MudElement HtmlTag="div" Class="mt-3">
|
||||||
|
<MudCheckBox Disabled="@(!_model.Transcode)" Label="Normalize Video" @bind-Checked="@_model.NormalizeVideo" For="@(() => _model.NormalizeVideo)"/>
|
||||||
|
</MudElement>
|
||||||
</MudItem>
|
</MudItem>
|
||||||
<MudItem>
|
<MudItem>
|
||||||
<MudText Typo="Typo.h6">Audio</MudText>
|
<MudText Typo="Typo.h6">Audio</MudText>
|
||||||
<MudTextField Disabled="@(!_model.Transcode)" Label="Codec" @bind-Value="_model.AudioCodec" For="@(() => _model.AudioCodec)"/>
|
<MudTextField Disabled="@(!_model.Transcode)" Label="Codec" @bind-Value="_model.AudioCodec" For="@(() => _model.AudioCodec)"/>
|
||||||
<MudElement HtmlTag="div" Class="mt-3">
|
<MudElement HtmlTag="div" Class="mt-3">
|
||||||
<MudTextField Disabled="@(!_model.Transcode)" Label="Bitrate" @bind-Value="_model.AudioBitrate" For="@(() => _model.AudioBitrate)" Adornment="Adornment.End" AdornmentText="kBit/s"/>
|
<MudTextField Disabled="@(!_model.Transcode)" Label="Bitrate" @bind-Value="_model.AudioBitrate" For="@(() => _model.AudioBitrate)" Adornment="Adornment.End" AdornmentText="kBit/s"/>
|
||||||
</MudElement>
|
</MudElement>
|
||||||
<MudElement HtmlTag="div" Class="mt-3">
|
<MudElement HtmlTag="div" Class="mt-3">
|
||||||
<MudTextField Disabled="@(!_model.Transcode)" Label="Buffer Size" @bind-Value="_model.AudioBufferSize" For="@(() => _model.AudioBufferSize)" Adornment="Adornment.End" AdornmentText="kBit"/>
|
<MudTextField Disabled="@(!_model.Transcode)" Label="Buffer Size" @bind-Value="_model.AudioBufferSize" For="@(() => _model.AudioBufferSize)" Adornment="Adornment.End" AdornmentText="kBit"/>
|
||||||
</MudElement>
|
</MudElement>
|
||||||
<MudElement HtmlTag="div" Class="mt-3">
|
|
||||||
<MudTextField Disabled="@(!_model.Transcode)" Label="Volume" @bind-Value="_model.AudioVolume" For="@(() => _model.AudioVolume)" Adornment="Adornment.End" AdornmentText="%"/>
|
|
||||||
</MudElement>
|
|
||||||
<MudElement HtmlTag="div" Class="mt-3">
|
<MudElement HtmlTag="div" Class="mt-3">
|
||||||
<MudTextField Disabled="@(!_model.Transcode)" Label="Channels" @bind-Value="_model.AudioChannels" For="@(() => _model.AudioChannels)"/>
|
<MudTextField Disabled="@(!_model.Transcode)" Label="Channels" @bind-Value="_model.AudioChannels" For="@(() => _model.AudioChannels)"/>
|
||||||
</MudElement>
|
</MudElement>
|
||||||
<MudElement HtmlTag="div" Class="mt-3">
|
<MudElement HtmlTag="div" Class="mt-3">
|
||||||
<MudTextField Disabled="@(!_model.Transcode)" Label="Sample Rate" @bind-Value="_model.AudioSampleRate" For="@(() => _model.AudioSampleRate)" Adornment="Adornment.End" AdornmentText="kHz"/>
|
<MudTextField Disabled="@(!_model.Transcode)" Label="Sample Rate" @bind-Value="_model.AudioSampleRate" For="@(() => _model.AudioSampleRate)" Adornment="Adornment.End" AdornmentText="kHz"/>
|
||||||
</MudElement>
|
</MudElement>
|
||||||
</MudItem>
|
|
||||||
<MudItem>
|
|
||||||
<MudText Typo="Typo.h6">Normalization</MudText>
|
|
||||||
<MudCheckBox Disabled="@(!_model.Transcode)" Label="Normalize Video" @bind-Checked="@_model.NormalizeVideo" For="@(() => _model.NormalizeVideo)"/>
|
|
||||||
<MudElement HtmlTag="div" Class="mt-3">
|
<MudElement HtmlTag="div" Class="mt-3">
|
||||||
<MudCheckBox Disabled="@(!_model.Transcode)" Label="Normalize Audio" @bind-Checked="@_model.NormalizeAudio" For="@(() => _model.NormalizeAudio)"/>
|
<MudCheckBox Disabled="@(!_model.Transcode)" Label="Normalize Audio" @bind-Checked="@_model.NormalizeAudio" For="@(() => _model.NormalizeAudio)"/>
|
||||||
</MudElement>
|
</MudElement>
|
||||||
|
<MudElement HtmlTag="div" Class="mt-3">
|
||||||
|
<MudCheckBox Disabled="@(!_model.Transcode)" Label="Normalize Loudness" @bind-Checked="@_model.NormalizeLoudness" For="@(() => _model.NormalizeLoudness)"/>
|
||||||
|
</MudElement>
|
||||||
</MudItem>
|
</MudItem>
|
||||||
</MudGrid>
|
</MudGrid>
|
||||||
</MudItem>
|
</MudItem>
|
||||||
|
|||||||
@@ -137,6 +137,7 @@
|
|||||||
{
|
{
|
||||||
<MediaCard Data="@card"
|
<MediaCard Data="@card"
|
||||||
Link=""
|
Link=""
|
||||||
|
ArtworkKind="ArtworkKind.Thumbnail"
|
||||||
AddToCollectionClicked="@AddToCollection"
|
AddToCollectionClicked="@AddToCollection"
|
||||||
SelectClicked="@(e => SelectClicked(card, e))"
|
SelectClicked="@(e => SelectClicked(card, e))"
|
||||||
IsSelected="@IsSelected(card)"
|
IsSelected="@IsSelected(card)"
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ namespace ErsatzTV.Validators
|
|||||||
|
|
||||||
RuleFor(x => x.AudioCodec).NotEmpty();
|
RuleFor(x => x.AudioCodec).NotEmpty();
|
||||||
RuleFor(x => x.AudioBitrate).GreaterThan(0);
|
RuleFor(x => x.AudioBitrate).GreaterThan(0);
|
||||||
RuleFor(x => x.AudioVolume).GreaterThanOrEqualTo(0);
|
|
||||||
RuleFor(x => x.AudioChannels).GreaterThan(0);
|
RuleFor(x => x.AudioChannels).GreaterThan(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ namespace ErsatzTV.ViewModels
|
|||||||
AudioChannels = viewModel.AudioChannels;
|
AudioChannels = viewModel.AudioChannels;
|
||||||
AudioCodec = viewModel.AudioCodec;
|
AudioCodec = viewModel.AudioCodec;
|
||||||
AudioSampleRate = viewModel.AudioSampleRate;
|
AudioSampleRate = viewModel.AudioSampleRate;
|
||||||
AudioVolume = viewModel.AudioVolume;
|
NormalizeLoudness = viewModel.NormalizeLoudness;
|
||||||
Id = viewModel.Id;
|
Id = viewModel.Id;
|
||||||
Name = viewModel.Name;
|
Name = viewModel.Name;
|
||||||
NormalizeAudio = viewModel.NormalizeAudio;
|
NormalizeAudio = viewModel.NormalizeAudio;
|
||||||
@@ -38,7 +38,7 @@ namespace ErsatzTV.ViewModels
|
|||||||
public int AudioChannels { get; set; }
|
public int AudioChannels { get; set; }
|
||||||
public string AudioCodec { get; set; }
|
public string AudioCodec { get; set; }
|
||||||
public int AudioSampleRate { get; set; }
|
public int AudioSampleRate { get; set; }
|
||||||
public int AudioVolume { get; set; }
|
public bool NormalizeLoudness { get; set; }
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
public bool NormalizeAudio { get; set; }
|
public bool NormalizeAudio { get; set; }
|
||||||
@@ -66,7 +66,7 @@ namespace ErsatzTV.ViewModels
|
|||||||
AudioCodec,
|
AudioCodec,
|
||||||
AudioBitrate,
|
AudioBitrate,
|
||||||
AudioBufferSize,
|
AudioBufferSize,
|
||||||
AudioVolume,
|
NormalizeLoudness,
|
||||||
AudioChannels,
|
AudioChannels,
|
||||||
AudioSampleRate,
|
AudioSampleRate,
|
||||||
NormalizeAudio,
|
NormalizeAudio,
|
||||||
@@ -88,7 +88,7 @@ namespace ErsatzTV.ViewModels
|
|||||||
AudioCodec,
|
AudioCodec,
|
||||||
AudioBitrate,
|
AudioBitrate,
|
||||||
AudioBufferSize,
|
AudioBufferSize,
|
||||||
AudioVolume,
|
NormalizeLoudness,
|
||||||
AudioChannels,
|
AudioChannels,
|
||||||
AudioSampleRate,
|
AudioSampleRate,
|
||||||
NormalizeAudio,
|
NormalizeAudio,
|
||||||
|
|||||||
Reference in New Issue
Block a user