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:
Jason Dove
2021-04-03 13:36:11 -05:00
committed by GitHub
parent d4a2197dfa
commit 74c95249c3
22 changed files with 2289 additions and 404 deletions
@@ -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,
@@ -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]
+2 -2
View File
@@ -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);
+3 -4
View File
@@ -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);
@@ -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
+1
View File
@@ -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))"
+7 -8
View File
@@ -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>
+1
View File
@@ -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,