浏览代码

Audio normalization (#9222)

Co-authored-by: Joe Rogers <1337joe@users.noreply.github.com>
Co-authored-by: Bond-009 <bond.009@outlook.com>
TelepathicWalrus 2 年之前
父节点
当前提交
603fce59df

+ 1 - 0
CONTRIBUTORS.md

@@ -126,6 +126,7 @@
  - [SuperSandro2000](https://github.com/SuperSandro2000)
  - [tbraeutigam](https://github.com/tbraeutigam)
  - [teacupx](https://github.com/teacupx)
+ - [TelepathicWalrus](https://github.com/TelepathicWalrus)
  - [Terror-Gene](https://github.com/Terror-Gene)
  - [ThatNerdyPikachu](https://github.com/ThatNerdyPikachu)
  - [ThibaultNocchi](https://github.com/ThibaultNocchi)

+ 10 - 2
Emby.Server.Implementations/Data/SqliteItemRepository.cs

@@ -49,8 +49,8 @@ namespace Emby.Server.Implementations.Data
 
         private const string SaveItemCommandText =
             @"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,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,@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,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)";
 
         private readonly IServerConfigurationManager _config;
         private readonly IServerApplicationHost _appHost;
@@ -110,6 +110,7 @@ namespace Emby.Server.Implementations.Data
             "PrimaryVersionId",
             "DateLastMediaAdded",
             "Album",
+            "LUFS",
             "CriticRating",
             "IsVirtualItem",
             "SeriesName",
@@ -489,6 +490,7 @@ namespace Emby.Server.Implementations.Data
                         AddColumn(db, "TypedBaseItems", "PrimaryVersionId", "Text", existingColumnNames);
                         AddColumn(db, "TypedBaseItems", "DateLastMediaAdded", "DATETIME", existingColumnNames);
                         AddColumn(db, "TypedBaseItems", "Album", "Text", existingColumnNames);
+                        AddColumn(db, "TypedBaseItems", "LUFS", "Float", existingColumnNames);
                         AddColumn(db, "TypedBaseItems", "IsVirtualItem", "BIT", existingColumnNames);
                         AddColumn(db, "TypedBaseItems", "SeriesName", "Text", existingColumnNames);
                         AddColumn(db, "TypedBaseItems", "UserDataKey", "Text", existingColumnNames);
@@ -906,6 +908,7 @@ namespace Emby.Server.Implementations.Data
             }
 
             saveItemStatement.TryBind("@Album", item.Album);
+            saveItemStatement.TryBind("@LUFS", item.LUFS);
             saveItemStatement.TryBind("@IsVirtualItem", item.IsVirtualItem);
 
             if (item is IHasSeries hasSeriesName)
@@ -1756,6 +1759,11 @@ namespace Emby.Server.Implementations.Data
                 item.Album = album;
             }
 
+            if (reader.TryGetSingle(index++, out var lUFS))
+            {
+                item.LUFS = lUFS;
+            }
+
             if (reader.TryGetSingle(index++, out var criticRating))
             {
                 item.CriticRating = criticRating;

+ 1 - 0
Emby.Server.Implementations/Dto/DtoService.cs

@@ -906,6 +906,7 @@ namespace Emby.Server.Implementations.Dto
             // Add audio info
             if (item is Audio audio)
             {
+                dto.LUFS = audio.LUFS;
                 dto.Album = audio.Album;
                 if (audio.ExtraType.HasValue)
                 {

+ 7 - 0
MediaBrowser.Controller/Entities/BaseItem.cs

@@ -128,6 +128,13 @@ namespace MediaBrowser.Controller.Entities
         [JsonIgnore]
         public string Album { get; set; }
 
+        /// <summary>
+        /// Gets or sets the LUFS value.
+        /// </summary>
+        /// <value>The LUFS Value.</value>
+        [JsonIgnore]
+        public float LUFS { get; set; }
+
         /// <summary>
         /// Gets or sets the channel identifier.
         /// </summary>

+ 2 - 0
MediaBrowser.Model/Configuration/LibraryOptions.cs

@@ -30,6 +30,8 @@ namespace MediaBrowser.Model.Configuration
 
         public bool EnableRealtimeMonitor { get; set; }
 
+        public bool EnableLUFSScan { get; set; }
+
         public bool EnableChapterImageExtraction { get; set; }
 
         public bool ExtractChapterImagesDuringLibraryScan { get; set; }

+ 6 - 0
MediaBrowser.Model/Dto/BaseItemDto.cs

@@ -779,6 +779,12 @@ namespace MediaBrowser.Model.Dto
         /// <value>The timer identifier.</value>
         public string TimerId { get; set; }
 
+        /// <summary>
+        /// Gets or sets the LUFS value.
+        /// </summary>
+        /// <value>The LUFS Value.</value>
+        public float LUFS { get; set; }
+
         /// <summary>
         /// Gets or sets the current program.
         /// </summary>

+ 60 - 0
MediaBrowser.Providers/MediaInfo/AudioFileProber.cs

@@ -1,6 +1,9 @@
 using System;
 using System.Collections.Generic;
+using System.Diagnostics;
+using System.Globalization;
 using System.Linq;
+using System.Text.RegularExpressions;
 using System.Threading;
 using System.Threading.Tasks;
 using Jellyfin.Data.Enums;
@@ -14,6 +17,7 @@ using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.MediaInfo;
+using Microsoft.Extensions.Logging;
 using TagLib;
 
 namespace MediaBrowser.Providers.MediaInfo
@@ -23,6 +27,10 @@ namespace MediaBrowser.Providers.MediaInfo
     /// </summary>
     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 IItemRepository _itemRepo;
         private readonly ILibraryManager _libraryManager;
@@ -31,16 +39,19 @@ namespace MediaBrowser.Providers.MediaInfo
         /// <summary>
         /// Initializes a new instance of the <see cref="AudioFileProber"/> class.
         /// </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="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
         /// <param name="itemRepo">Instance of the <see cref="IItemRepository"/> interface.</param>
         /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
         public AudioFileProber(
+            ILogger<AudioFileProber> logger,
             IMediaSourceManager mediaSourceManager,
             IMediaEncoder mediaEncoder,
             IItemRepository itemRepo,
             ILibraryManager libraryManager)
         {
+            _logger = logger;
             _mediaEncoder = mediaEncoder;
             _itemRepo = itemRepo;
             _libraryManager = libraryManager;
@@ -89,6 +100,54 @@ namespace MediaBrowser.Providers.MediaInfo
                 Fetch(item, result, cancellationToken);
             }
 
+            var libraryOptions = _libraryManager.GetLibraryOptions(item);
+
+            if (libraryOptions.EnableLUFSScan)
+            {
+                string output;
+                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;
+                    }
+
+                    output = await process.StandardError.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
+                    cancellationToken.ThrowIfCancellationRequested();
+                    MatchCollection split = Regex.Matches(output, @"I:\s+(.*?)\s+LUFS");
+
+                    if (split.Count != 0)
+                    {
+                        item.LUFS = float.Parse(split[0].Groups[1].ValueSpan, CultureInfo.InvariantCulture.NumberFormat);
+                    }
+                    else
+                    {
+                        item.LUFS = DefaultLUFSValue;
+                    }
+                }
+            }
+            else
+            {
+                item.LUFS = DefaultLUFSValue;
+            }
+
+            _logger.LogDebug("LUFS for {ItemName} is {LUFS}.", item.Name, item.LUFS);
+
             return ItemUpdateType.MetadataImport;
         }
 
@@ -196,6 +255,7 @@ namespace MediaBrowser.Providers.MediaInfo
                 audio.Album = tags.Album;
                 audio.IndexNumber = Convert.ToInt32(tags.Track);
                 audio.ParentIndexNumber = Convert.ToInt32(tags.Disc);
+
                 if (tags.Year != 0)
                 {
                     var year = Convert.ToInt32(tags.Year);

+ 1 - 1
MediaBrowser.Providers/MediaInfo/ProbeProvider.cs

@@ -79,7 +79,7 @@ namespace MediaBrowser.Providers.MediaInfo
             NamingOptions namingOptions)
         {
             _logger = loggerFactory.CreateLogger<ProbeProvider>();
-            _audioProber = new AudioFileProber(mediaSourceManager, mediaEncoder, itemRepo, libraryManager);
+            _audioProber = new AudioFileProber(loggerFactory.CreateLogger<AudioFileProber>(), mediaSourceManager, mediaEncoder, itemRepo, libraryManager);
             _audioResolver = new AudioResolver(loggerFactory.CreateLogger<AudioResolver>(), localization, mediaEncoder, fileSystem, namingOptions);
             _subtitleResolver = new SubtitleResolver(loggerFactory.CreateLogger<SubtitleResolver>(), localization, mediaEncoder, fileSystem, namingOptions);
             _videoProber = new FFProbeVideoInfo(