mirror of
https://github.com/jellyfin/jellyfin.git
synced 2025-05-24 02:02:29 -04:00
Improve audio normalization
* Move calculation of LUFS to a scheduled task as it's pretty slow * Correctly calculate album LUFS * Don't try to convert replaygain tags to LUFS values
This commit is contained in:
parent
5612cb8178
commit
88a38a61b5
@ -49,8 +49,8 @@ namespace Emby.Server.Implementations.Data
|
|||||||
|
|
||||||
private const string SaveItemCommandText =
|
private const string SaveItemCommandText =
|
||||||
@"replace into TypedBaseItems
|
@"replace into TypedBaseItems
|
||||||
(guid,type,data,Path,StartDate,EndDate,ChannelId,IsMovie,IsSeries,EpisodeTitle,IsRepeat,CommunityRating,CustomRating,IndexNumber,IsLocked,Name,OfficialRating,MediaType,Overview,ParentIndexNumber,PremiereDate,ProductionYear,ParentId,Genres,InheritedParentalRatingValue,SortName,ForcedSortName,RunTimeTicks,Size,DateCreated,DateModified,PreferredMetadataLanguage,PreferredMetadataCountryCode,Width,Height,DateLastRefreshed,DateLastSaved,IsInMixedFolder,LockedFields,Studios,Audio,ExternalServiceId,Tags,IsFolder,UnratedType,TopParentId,TrailerTypes,CriticRating,CleanName,PresentationUniqueKey,OriginalTitle,PrimaryVersionId,DateLastMediaAdded,Album,LUFS,IsVirtualItem,SeriesName,UserDataKey,SeasonName,SeasonId,SeriesId,ExternalSeriesId,Tagline,ProviderIds,Images,ProductionLocations,ExtraIds,TotalBitrate,ExtraType,Artists,AlbumArtists,ExternalId,SeriesPresentationUniqueKey,ShowId,OwnerId)
|
(guid,type,data,Path,StartDate,EndDate,ChannelId,IsMovie,IsSeries,EpisodeTitle,IsRepeat,CommunityRating,CustomRating,IndexNumber,IsLocked,Name,OfficialRating,MediaType,Overview,ParentIndexNumber,PremiereDate,ProductionYear,ParentId,Genres,InheritedParentalRatingValue,SortName,ForcedSortName,RunTimeTicks,Size,DateCreated,DateModified,PreferredMetadataLanguage,PreferredMetadataCountryCode,Width,Height,DateLastRefreshed,DateLastSaved,IsInMixedFolder,LockedFields,Studios,Audio,ExternalServiceId,Tags,IsFolder,UnratedType,TopParentId,TrailerTypes,CriticRating,CleanName,PresentationUniqueKey,OriginalTitle,PrimaryVersionId,DateLastMediaAdded,Album,LUFS,NormalizationGain,IsVirtualItem,SeriesName,UserDataKey,SeasonName,SeasonId,SeriesId,ExternalSeriesId,Tagline,ProviderIds,Images,ProductionLocations,ExtraIds,TotalBitrate,ExtraType,Artists,AlbumArtists,ExternalId,SeriesPresentationUniqueKey,ShowId,OwnerId)
|
||||||
values (@guid,@type,@data,@Path,@StartDate,@EndDate,@ChannelId,@IsMovie,@IsSeries,@EpisodeTitle,@IsRepeat,@CommunityRating,@CustomRating,@IndexNumber,@IsLocked,@Name,@OfficialRating,@MediaType,@Overview,@ParentIndexNumber,@PremiereDate,@ProductionYear,@ParentId,@Genres,@InheritedParentalRatingValue,@SortName,@ForcedSortName,@RunTimeTicks,@Size,@DateCreated,@DateModified,@PreferredMetadataLanguage,@PreferredMetadataCountryCode,@Width,@Height,@DateLastRefreshed,@DateLastSaved,@IsInMixedFolder,@LockedFields,@Studios,@Audio,@ExternalServiceId,@Tags,@IsFolder,@UnratedType,@TopParentId,@TrailerTypes,@CriticRating,@CleanName,@PresentationUniqueKey,@OriginalTitle,@PrimaryVersionId,@DateLastMediaAdded,@Album,@LUFS,@IsVirtualItem,@SeriesName,@UserDataKey,@SeasonName,@SeasonId,@SeriesId,@ExternalSeriesId,@Tagline,@ProviderIds,@Images,@ProductionLocations,@ExtraIds,@TotalBitrate,@ExtraType,@Artists,@AlbumArtists,@ExternalId,@SeriesPresentationUniqueKey,@ShowId,@OwnerId)";
|
values (@guid,@type,@data,@Path,@StartDate,@EndDate,@ChannelId,@IsMovie,@IsSeries,@EpisodeTitle,@IsRepeat,@CommunityRating,@CustomRating,@IndexNumber,@IsLocked,@Name,@OfficialRating,@MediaType,@Overview,@ParentIndexNumber,@PremiereDate,@ProductionYear,@ParentId,@Genres,@InheritedParentalRatingValue,@SortName,@ForcedSortName,@RunTimeTicks,@Size,@DateCreated,@DateModified,@PreferredMetadataLanguage,@PreferredMetadataCountryCode,@Width,@Height,@DateLastRefreshed,@DateLastSaved,@IsInMixedFolder,@LockedFields,@Studios,@Audio,@ExternalServiceId,@Tags,@IsFolder,@UnratedType,@TopParentId,@TrailerTypes,@CriticRating,@CleanName,@PresentationUniqueKey,@OriginalTitle,@PrimaryVersionId,@DateLastMediaAdded,@Album,@LUFS,@NormalizationGain,@IsVirtualItem,@SeriesName,@UserDataKey,@SeasonName,@SeasonId,@SeriesId,@ExternalSeriesId,@Tagline,@ProviderIds,@Images,@ProductionLocations,@ExtraIds,@TotalBitrate,@ExtraType,@Artists,@AlbumArtists,@ExternalId,@SeriesPresentationUniqueKey,@ShowId,@OwnerId)";
|
||||||
|
|
||||||
private readonly IServerConfigurationManager _config;
|
private readonly IServerConfigurationManager _config;
|
||||||
private readonly IServerApplicationHost _appHost;
|
private readonly IServerApplicationHost _appHost;
|
||||||
@ -111,6 +111,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
"DateLastMediaAdded",
|
"DateLastMediaAdded",
|
||||||
"Album",
|
"Album",
|
||||||
"LUFS",
|
"LUFS",
|
||||||
|
"NormalizationGain",
|
||||||
"CriticRating",
|
"CriticRating",
|
||||||
"IsVirtualItem",
|
"IsVirtualItem",
|
||||||
"SeriesName",
|
"SeriesName",
|
||||||
@ -478,6 +479,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
AddColumn(connection, "TypedBaseItems", "DateLastMediaAdded", "DATETIME", existingColumnNames);
|
AddColumn(connection, "TypedBaseItems", "DateLastMediaAdded", "DATETIME", existingColumnNames);
|
||||||
AddColumn(connection, "TypedBaseItems", "Album", "Text", existingColumnNames);
|
AddColumn(connection, "TypedBaseItems", "Album", "Text", existingColumnNames);
|
||||||
AddColumn(connection, "TypedBaseItems", "LUFS", "Float", existingColumnNames);
|
AddColumn(connection, "TypedBaseItems", "LUFS", "Float", existingColumnNames);
|
||||||
|
AddColumn(connection, "TypedBaseItems", "NormalizationGain", "Float", existingColumnNames);
|
||||||
AddColumn(connection, "TypedBaseItems", "IsVirtualItem", "BIT", existingColumnNames);
|
AddColumn(connection, "TypedBaseItems", "IsVirtualItem", "BIT", existingColumnNames);
|
||||||
AddColumn(connection, "TypedBaseItems", "SeriesName", "Text", existingColumnNames);
|
AddColumn(connection, "TypedBaseItems", "SeriesName", "Text", existingColumnNames);
|
||||||
AddColumn(connection, "TypedBaseItems", "UserDataKey", "Text", existingColumnNames);
|
AddColumn(connection, "TypedBaseItems", "UserDataKey", "Text", existingColumnNames);
|
||||||
@ -886,6 +888,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
|
|
||||||
saveItemStatement.TryBind("@Album", item.Album);
|
saveItemStatement.TryBind("@Album", item.Album);
|
||||||
saveItemStatement.TryBind("@LUFS", item.LUFS);
|
saveItemStatement.TryBind("@LUFS", item.LUFS);
|
||||||
|
saveItemStatement.TryBind("@NormalizationGain", item.NormalizationGain);
|
||||||
saveItemStatement.TryBind("@IsVirtualItem", item.IsVirtualItem);
|
saveItemStatement.TryBind("@IsVirtualItem", item.IsVirtualItem);
|
||||||
|
|
||||||
if (item is IHasSeries hasSeriesName)
|
if (item is IHasSeries hasSeriesName)
|
||||||
@ -1672,6 +1675,11 @@ namespace Emby.Server.Implementations.Data
|
|||||||
item.LUFS = lUFS;
|
item.LUFS = lUFS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (reader.TryGetSingle(index++, out var normalizationGain))
|
||||||
|
{
|
||||||
|
item.NormalizationGain = normalizationGain;
|
||||||
|
}
|
||||||
|
|
||||||
if (reader.TryGetSingle(index++, out var criticRating))
|
if (reader.TryGetSingle(index++, out var criticRating))
|
||||||
{
|
{
|
||||||
item.CriticRating = criticRating;
|
item.CriticRating = criticRating;
|
||||||
|
@ -898,7 +898,14 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
dto.IsPlaceHolder = supportsPlaceHolders.IsPlaceHolder;
|
dto.IsPlaceHolder = supportsPlaceHolders.IsPlaceHolder;
|
||||||
}
|
}
|
||||||
|
|
||||||
dto.LUFS = item.LUFS;
|
if (item.LUFS.HasValue)
|
||||||
|
{
|
||||||
|
dto.NormalizationGain = -18f - item.LUFS;
|
||||||
|
}
|
||||||
|
else if (item.NormalizationGain.HasValue)
|
||||||
|
{
|
||||||
|
dto.NormalizationGain = item.NormalizationGain;
|
||||||
|
}
|
||||||
|
|
||||||
// Add audio info
|
// Add audio info
|
||||||
if (item is Audio audio)
|
if (item is Audio audio)
|
||||||
|
@ -13,7 +13,6 @@ using MediaBrowser.Controller.Entities.Audio;
|
|||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Providers;
|
using MediaBrowser.Controller.Providers;
|
||||||
using MediaBrowser.Controller.Resolvers;
|
using MediaBrowser.Controller.Resolvers;
|
||||||
using MediaBrowser.Model.Entities;
|
|
||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
@ -106,6 +106,8 @@
|
|||||||
"TaskCleanCacheDescription": "Deletes cache files no longer needed by the system.",
|
"TaskCleanCacheDescription": "Deletes cache files no longer needed by the system.",
|
||||||
"TaskRefreshChapterImages": "Extract Chapter Images",
|
"TaskRefreshChapterImages": "Extract Chapter Images",
|
||||||
"TaskRefreshChapterImagesDescription": "Creates thumbnails for videos that have chapters.",
|
"TaskRefreshChapterImagesDescription": "Creates thumbnails for videos that have chapters.",
|
||||||
|
"TaskAudioNormalization": "Audio Normalization",
|
||||||
|
"TaskAudioNormalizationDescription": "Scans files for audio normalization data.",
|
||||||
"TaskRefreshLibrary": "Scan Media Library",
|
"TaskRefreshLibrary": "Scan Media Library",
|
||||||
"TaskRefreshLibraryDescription": "Scans your media library for new files and refreshes metadata.",
|
"TaskRefreshLibraryDescription": "Scans your media library for new files and refreshes metadata.",
|
||||||
"TaskCleanLogs": "Clean Log Directory",
|
"TaskCleanLogs": "Clean Log Directory",
|
||||||
|
@ -0,0 +1,196 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Jellyfin.Data.Enums;
|
||||||
|
using MediaBrowser.Common.Configuration;
|
||||||
|
using MediaBrowser.Controller.Entities;
|
||||||
|
using MediaBrowser.Controller.Entities.Audio;
|
||||||
|
using MediaBrowser.Controller.Library;
|
||||||
|
using MediaBrowser.Controller.MediaEncoding;
|
||||||
|
using MediaBrowser.Controller.Persistence;
|
||||||
|
using MediaBrowser.Model.Globalization;
|
||||||
|
using MediaBrowser.Model.Tasks;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Emby.Server.Implementations.ScheduledTasks.Tasks;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The splashscreen post scan task.
|
||||||
|
/// </summary>
|
||||||
|
public partial class AudioNormalizationTask : IScheduledTask
|
||||||
|
{
|
||||||
|
private readonly IItemRepository _itemRepository;
|
||||||
|
private readonly ILibraryManager _libraryManager;
|
||||||
|
private readonly IMediaEncoder _mediaEncoder;
|
||||||
|
private readonly IConfigurationManager _configurationManager;
|
||||||
|
private readonly ILocalizationManager _localization;
|
||||||
|
private readonly ILogger<AudioNormalizationTask> _logger;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="AudioNormalizationTask"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="itemRepository">Instance of the <see cref="IItemRepository"/> interface.</param>
|
||||||
|
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||||
|
/// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
|
||||||
|
/// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
|
||||||
|
/// <param name="localizationManager">Instance of the <see cref="ILocalizationManager"/> interface.</param>
|
||||||
|
/// <param name="logger">Instance of the <see cref="ILogger{AudioNormalizationTask}"/> interface.</param>
|
||||||
|
public AudioNormalizationTask(
|
||||||
|
IItemRepository itemRepository,
|
||||||
|
ILibraryManager libraryManager,
|
||||||
|
IMediaEncoder mediaEncoder,
|
||||||
|
IConfigurationManager configurationManager,
|
||||||
|
ILocalizationManager localizationManager,
|
||||||
|
ILogger<AudioNormalizationTask> logger)
|
||||||
|
{
|
||||||
|
_itemRepository = itemRepository;
|
||||||
|
_libraryManager = libraryManager;
|
||||||
|
_mediaEncoder = mediaEncoder;
|
||||||
|
_configurationManager = configurationManager;
|
||||||
|
_localization = localizationManager;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string Name => _localization.GetLocalizedString("TaskAudioNormalization");
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string Description => _localization.GetLocalizedString("TaskAudioNormalizationDescription");
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string Category => _localization.GetLocalizedString("TasksLibraryCategory");
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string Key => "AudioNormalization";
|
||||||
|
|
||||||
|
[GeneratedRegex(@"I:\s+(.*?)\s+LUFS")]
|
||||||
|
private static partial Regex LUFSRegex();
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
foreach (var library in _libraryManager.RootFolder.Children)
|
||||||
|
{
|
||||||
|
var libraryOptions = _libraryManager.GetLibraryOptions(library);
|
||||||
|
if (!libraryOptions.EnableLUFSScan)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Album gain
|
||||||
|
var albums = _libraryManager.GetItemList(new InternalItemsQuery
|
||||||
|
{
|
||||||
|
IncludeItemTypes = [BaseItemKind.MusicAlbum],
|
||||||
|
Parent = library,
|
||||||
|
Recursive = true
|
||||||
|
});
|
||||||
|
|
||||||
|
foreach (var a in albums)
|
||||||
|
{
|
||||||
|
if (a.NormalizationGain.HasValue || a.LUFS.HasValue)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var albumTracks = ((MusicAlbum)a).Tracks.Where(x => x.IsFileProtocol).ToList();
|
||||||
|
if (albumTracks.Count == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tempFile = Path.Join(_configurationManager.GetTranscodePath(), Guid.NewGuid() + ".concat");
|
||||||
|
var inputLines = albumTracks.Select(x => string.Format(CultureInfo.InvariantCulture, "file '{0}'", x.Path.Replace("'", @"'\''", StringComparison.Ordinal)));
|
||||||
|
await File.WriteAllLinesAsync(tempFile, inputLines, cancellationToken).ConfigureAwait(false);
|
||||||
|
a.LUFS = await CalculateLUFSAsync(
|
||||||
|
string.Format(CultureInfo.InvariantCulture, "-f concat -safe 0 -i \"{0}\"", tempFile),
|
||||||
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
File.Delete(tempFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
_itemRepository.SaveItems(albums, cancellationToken);
|
||||||
|
|
||||||
|
var tracks = _libraryManager.GetItemList(new InternalItemsQuery
|
||||||
|
{
|
||||||
|
MediaTypes = [MediaType.Audio],
|
||||||
|
IncludeItemTypes = [BaseItemKind.Audio],
|
||||||
|
Parent = library,
|
||||||
|
Recursive = true
|
||||||
|
});
|
||||||
|
|
||||||
|
foreach (var t in tracks)
|
||||||
|
{
|
||||||
|
if (t.NormalizationGain.HasValue || t.LUFS.HasValue || !t.IsFileProtocol)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
t.LUFS = await CalculateLUFSAsync(string.Format(CultureInfo.InvariantCulture, "-i \"{0}\"", t.Path.Replace("\"", "\\\"", StringComparison.Ordinal)), cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
_itemRepository.SaveItems(tracks, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
|
||||||
|
{
|
||||||
|
return
|
||||||
|
[
|
||||||
|
new TaskTriggerInfo
|
||||||
|
{
|
||||||
|
Type = TaskTriggerInfo.TriggerInterval,
|
||||||
|
IntervalTicks = TimeSpan.FromHours(24).Ticks
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private string EscapeFilename(string filename)
|
||||||
|
=> filename;
|
||||||
|
|
||||||
|
private async Task<float?> CalculateLUFSAsync(string inputArgs, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var args = $"-hide_banner {inputArgs} -af ebur128=framelog=verbose -f null -";
|
||||||
|
|
||||||
|
using (var process = new Process()
|
||||||
|
{
|
||||||
|
StartInfo = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = _mediaEncoder.EncoderPath,
|
||||||
|
Arguments = args,
|
||||||
|
RedirectStandardOutput = false,
|
||||||
|
RedirectStandardError = true
|
||||||
|
},
|
||||||
|
})
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Starting ffmpeg with arguments: {Arguments}", args);
|
||||||
|
process.Start();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error starting ffmpeg with arguments: {Arguments}", args);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var reader = process.StandardError;
|
||||||
|
var output = await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
MatchCollection split = LUFSRegex().Matches(output);
|
||||||
|
|
||||||
|
if (split.Count != 0)
|
||||||
|
{
|
||||||
|
return float.Parse(split[0].Groups[1].ValueSpan, CultureInfo.InvariantCulture.NumberFormat);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogError("Failed to find LUFS value in output:\n{Output}", output);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -13,7 +13,6 @@ using MediaBrowser.Controller.Library;
|
|||||||
using MediaBrowser.Controller.MediaEncoding;
|
using MediaBrowser.Controller.MediaEncoding;
|
||||||
using MediaBrowser.Controller.Persistence;
|
using MediaBrowser.Controller.Persistence;
|
||||||
using MediaBrowser.Controller.Providers;
|
using MediaBrowser.Controller.Providers;
|
||||||
using MediaBrowser.Model.Entities;
|
|
||||||
using MediaBrowser.Model.Globalization;
|
using MediaBrowser.Model.Globalization;
|
||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
using MediaBrowser.Model.Tasks;
|
using MediaBrowser.Model.Tasks;
|
||||||
|
@ -183,14 +183,13 @@ namespace MediaBrowser.Controller.Entities.Audio
|
|||||||
progress.Report(percent * 95);
|
progress.Report(percent * 95);
|
||||||
}
|
}
|
||||||
|
|
||||||
// get album LUFS
|
|
||||||
LUFS = items.OfType<Audio>().Max(item => item.LUFS);
|
|
||||||
|
|
||||||
var parentRefreshOptions = refreshOptions;
|
var parentRefreshOptions = refreshOptions;
|
||||||
if (childUpdateType > ItemUpdateType.None)
|
if (childUpdateType > ItemUpdateType.None)
|
||||||
{
|
{
|
||||||
parentRefreshOptions = new MetadataRefreshOptions(refreshOptions);
|
parentRefreshOptions = new MetadataRefreshOptions(refreshOptions)
|
||||||
parentRefreshOptions.MetadataRefreshMode = MetadataRefreshMode.FullRefresh;
|
{
|
||||||
|
MetadataRefreshMode = MetadataRefreshMode.FullRefresh
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh current item
|
// Refresh current item
|
||||||
|
@ -137,6 +137,13 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public float? LUFS { get; set; }
|
public float? LUFS { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the gain required for audio normalization.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The gain required for audio normalization..</value>
|
||||||
|
[JsonIgnore]
|
||||||
|
public float? NormalizationGain { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the channel identifier.
|
/// Gets or sets the channel identifier.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -782,10 +782,10 @@ namespace MediaBrowser.Model.Dto
|
|||||||
public string TimerId { get; set; }
|
public string TimerId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the LUFS value.
|
/// Gets or sets the gain required for audio normalization.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The LUFS Value.</value>
|
/// <value>The gain required for audio normalization..</value>
|
||||||
public float? LUFS { get; set; }
|
public float? NormalizationGain { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the current program.
|
/// Gets or sets the current program.
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
@ -18,7 +15,6 @@ using MediaBrowser.Model.Dlna;
|
|||||||
using MediaBrowser.Model.Dto;
|
using MediaBrowser.Model.Dto;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
using MediaBrowser.Model.MediaInfo;
|
using MediaBrowser.Model.MediaInfo;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using TagLib;
|
using TagLib;
|
||||||
|
|
||||||
namespace MediaBrowser.Providers.MediaInfo
|
namespace MediaBrowser.Providers.MediaInfo
|
||||||
@ -26,12 +22,8 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Probes audio files for metadata.
|
/// Probes audio files for metadata.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class AudioFileProber
|
public class AudioFileProber
|
||||||
{
|
{
|
||||||
// Default LUFS value for use with the web interface, at -18db gain will be 1(no db gain).
|
|
||||||
private const float DefaultLUFSValue = -18;
|
|
||||||
|
|
||||||
private readonly ILogger<AudioFileProber> _logger;
|
|
||||||
private readonly IMediaEncoder _mediaEncoder;
|
private readonly IMediaEncoder _mediaEncoder;
|
||||||
private readonly IItemRepository _itemRepo;
|
private readonly IItemRepository _itemRepo;
|
||||||
private readonly ILibraryManager _libraryManager;
|
private readonly ILibraryManager _libraryManager;
|
||||||
@ -42,7 +34,6 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="AudioFileProber"/> class.
|
/// Initializes a new instance of the <see cref="AudioFileProber"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
|
|
||||||
/// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
|
/// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
|
||||||
/// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
|
/// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
|
||||||
/// <param name="itemRepo">Instance of the <see cref="IItemRepository"/> interface.</param>
|
/// <param name="itemRepo">Instance of the <see cref="IItemRepository"/> interface.</param>
|
||||||
@ -50,7 +41,6 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
/// <param name="lyricResolver">Instance of the <see cref="LyricResolver"/> interface.</param>
|
/// <param name="lyricResolver">Instance of the <see cref="LyricResolver"/> interface.</param>
|
||||||
/// <param name="lyricManager">Instance of the <see cref="ILyricManager"/> interface.</param>
|
/// <param name="lyricManager">Instance of the <see cref="ILyricManager"/> interface.</param>
|
||||||
public AudioFileProber(
|
public AudioFileProber(
|
||||||
ILogger<AudioFileProber> logger,
|
|
||||||
IMediaSourceManager mediaSourceManager,
|
IMediaSourceManager mediaSourceManager,
|
||||||
IMediaEncoder mediaEncoder,
|
IMediaEncoder mediaEncoder,
|
||||||
IItemRepository itemRepo,
|
IItemRepository itemRepo,
|
||||||
@ -58,7 +48,6 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
LyricResolver lyricResolver,
|
LyricResolver lyricResolver,
|
||||||
ILyricManager lyricManager)
|
ILyricManager lyricManager)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
|
||||||
_mediaEncoder = mediaEncoder;
|
_mediaEncoder = mediaEncoder;
|
||||||
_itemRepo = itemRepo;
|
_itemRepo = itemRepo;
|
||||||
_libraryManager = libraryManager;
|
_libraryManager = libraryManager;
|
||||||
@ -67,9 +56,6 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
_lyricManager = lyricManager;
|
_lyricManager = lyricManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
[GeneratedRegex(@"I:\s+(.*?)\s+LUFS")]
|
|
||||||
private static partial Regex LUFSRegex();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Probes the specified item for metadata.
|
/// Probes the specified item for metadata.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -112,45 +98,6 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
await FetchAsync(item, result, options, cancellationToken).ConfigureAwait(false);
|
await FetchAsync(item, result, options, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
var libraryOptions = _libraryManager.GetLibraryOptions(item);
|
|
||||||
if (libraryOptions.EnableLUFSScan && item.LUFS is null)
|
|
||||||
{
|
|
||||||
using (var process = new Process()
|
|
||||||
{
|
|
||||||
StartInfo = new ProcessStartInfo
|
|
||||||
{
|
|
||||||
FileName = _mediaEncoder.EncoderPath,
|
|
||||||
Arguments = $"-hide_banner -i \"{path}\" -af ebur128=framelog=verbose -f null -",
|
|
||||||
RedirectStandardOutput = false,
|
|
||||||
RedirectStandardError = true
|
|
||||||
},
|
|
||||||
})
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
process.Start();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error starting ffmpeg");
|
|
||||||
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
|
|
||||||
using var reader = process.StandardError;
|
|
||||||
var output = await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
MatchCollection split = LUFSRegex().Matches(output);
|
|
||||||
|
|
||||||
if (split.Count != 0)
|
|
||||||
{
|
|
||||||
item.LUFS = float.Parse(split[0].Groups[1].ValueSpan, CultureInfo.InvariantCulture.NumberFormat);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogDebug("LUFS for {ItemName} is {LUFS}.", item.Name, item.LUFS);
|
|
||||||
|
|
||||||
return ItemUpdateType.MetadataImport;
|
return ItemUpdateType.MetadataImport;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -339,7 +286,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
|
|
||||||
if (!double.IsNaN(tags.ReplayGainTrackGain))
|
if (!double.IsNaN(tags.ReplayGainTrackGain))
|
||||||
{
|
{
|
||||||
audio.LUFS = DefaultLUFSValue - (float)tags.ReplayGainTrackGain;
|
audio.NormalizationGain = (float)tags.ReplayGainTrackGain;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzArtist, out _))
|
if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzArtist, out _))
|
||||||
|
@ -103,7 +103,6 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
_subtitleResolver);
|
_subtitleResolver);
|
||||||
|
|
||||||
_audioProber = new AudioFileProber(
|
_audioProber = new AudioFileProber(
|
||||||
loggerFactory.CreateLogger<AudioFileProber>(),
|
|
||||||
mediaSourceManager,
|
mediaSourceManager,
|
||||||
mediaEncoder,
|
mediaEncoder,
|
||||||
itemRepo,
|
itemRepo,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user