mirror of
https://github.com/jellyfin/jellyfin.git
synced 2024-06-29 02:13:36 +02:00
Audio normalization (#9222)
Co-authored-by: Joe Rogers <1337joe@users.noreply.github.com> Co-authored-by: Bond-009 <bond.009@outlook.com>
This commit is contained in:
parent
47290a8c36
commit
603fce59df
|
@ -126,6 +126,7 @@
|
||||||
- [SuperSandro2000](https://github.com/SuperSandro2000)
|
- [SuperSandro2000](https://github.com/SuperSandro2000)
|
||||||
- [tbraeutigam](https://github.com/tbraeutigam)
|
- [tbraeutigam](https://github.com/tbraeutigam)
|
||||||
- [teacupx](https://github.com/teacupx)
|
- [teacupx](https://github.com/teacupx)
|
||||||
|
- [TelepathicWalrus](https://github.com/TelepathicWalrus)
|
||||||
- [Terror-Gene](https://github.com/Terror-Gene)
|
- [Terror-Gene](https://github.com/Terror-Gene)
|
||||||
- [ThatNerdyPikachu](https://github.com/ThatNerdyPikachu)
|
- [ThatNerdyPikachu](https://github.com/ThatNerdyPikachu)
|
||||||
- [ThibaultNocchi](https://github.com/ThibaultNocchi)
|
- [ThibaultNocchi](https://github.com/ThibaultNocchi)
|
||||||
|
|
|
@ -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,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,@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 IServerConfigurationManager _config;
|
||||||
private readonly IServerApplicationHost _appHost;
|
private readonly IServerApplicationHost _appHost;
|
||||||
|
@ -110,6 +110,7 @@ namespace Emby.Server.Implementations.Data
|
||||||
"PrimaryVersionId",
|
"PrimaryVersionId",
|
||||||
"DateLastMediaAdded",
|
"DateLastMediaAdded",
|
||||||
"Album",
|
"Album",
|
||||||
|
"LUFS",
|
||||||
"CriticRating",
|
"CriticRating",
|
||||||
"IsVirtualItem",
|
"IsVirtualItem",
|
||||||
"SeriesName",
|
"SeriesName",
|
||||||
|
@ -489,6 +490,7 @@ namespace Emby.Server.Implementations.Data
|
||||||
AddColumn(db, "TypedBaseItems", "PrimaryVersionId", "Text", existingColumnNames);
|
AddColumn(db, "TypedBaseItems", "PrimaryVersionId", "Text", existingColumnNames);
|
||||||
AddColumn(db, "TypedBaseItems", "DateLastMediaAdded", "DATETIME", existingColumnNames);
|
AddColumn(db, "TypedBaseItems", "DateLastMediaAdded", "DATETIME", existingColumnNames);
|
||||||
AddColumn(db, "TypedBaseItems", "Album", "Text", existingColumnNames);
|
AddColumn(db, "TypedBaseItems", "Album", "Text", existingColumnNames);
|
||||||
|
AddColumn(db, "TypedBaseItems", "LUFS", "Float", existingColumnNames);
|
||||||
AddColumn(db, "TypedBaseItems", "IsVirtualItem", "BIT", existingColumnNames);
|
AddColumn(db, "TypedBaseItems", "IsVirtualItem", "BIT", existingColumnNames);
|
||||||
AddColumn(db, "TypedBaseItems", "SeriesName", "Text", existingColumnNames);
|
AddColumn(db, "TypedBaseItems", "SeriesName", "Text", existingColumnNames);
|
||||||
AddColumn(db, "TypedBaseItems", "UserDataKey", "Text", existingColumnNames);
|
AddColumn(db, "TypedBaseItems", "UserDataKey", "Text", existingColumnNames);
|
||||||
|
@ -906,6 +908,7 @@ namespace Emby.Server.Implementations.Data
|
||||||
}
|
}
|
||||||
|
|
||||||
saveItemStatement.TryBind("@Album", item.Album);
|
saveItemStatement.TryBind("@Album", item.Album);
|
||||||
|
saveItemStatement.TryBind("@LUFS", item.LUFS);
|
||||||
saveItemStatement.TryBind("@IsVirtualItem", item.IsVirtualItem);
|
saveItemStatement.TryBind("@IsVirtualItem", item.IsVirtualItem);
|
||||||
|
|
||||||
if (item is IHasSeries hasSeriesName)
|
if (item is IHasSeries hasSeriesName)
|
||||||
|
@ -1756,6 +1759,11 @@ namespace Emby.Server.Implementations.Data
|
||||||
item.Album = album;
|
item.Album = album;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (reader.TryGetSingle(index++, out var lUFS))
|
||||||
|
{
|
||||||
|
item.LUFS = lUFS;
|
||||||
|
}
|
||||||
|
|
||||||
if (reader.TryGetSingle(index++, out var criticRating))
|
if (reader.TryGetSingle(index++, out var criticRating))
|
||||||
{
|
{
|
||||||
item.CriticRating = criticRating;
|
item.CriticRating = criticRating;
|
||||||
|
|
|
@ -906,6 +906,7 @@ namespace Emby.Server.Implementations.Dto
|
||||||
// Add audio info
|
// Add audio info
|
||||||
if (item is Audio audio)
|
if (item is Audio audio)
|
||||||
{
|
{
|
||||||
|
dto.LUFS = audio.LUFS;
|
||||||
dto.Album = audio.Album;
|
dto.Album = audio.Album;
|
||||||
if (audio.ExtraType.HasValue)
|
if (audio.ExtraType.HasValue)
|
||||||
{
|
{
|
||||||
|
|
|
@ -128,6 +128,13 @@ namespace MediaBrowser.Controller.Entities
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public string Album { get; set; }
|
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>
|
/// <summary>
|
||||||
/// Gets or sets the channel identifier.
|
/// Gets or sets the channel identifier.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
@ -30,6 +30,8 @@ namespace MediaBrowser.Model.Configuration
|
||||||
|
|
||||||
public bool EnableRealtimeMonitor { get; set; }
|
public bool EnableRealtimeMonitor { get; set; }
|
||||||
|
|
||||||
|
public bool EnableLUFSScan { get; set; }
|
||||||
|
|
||||||
public bool EnableChapterImageExtraction { get; set; }
|
public bool EnableChapterImageExtraction { get; set; }
|
||||||
|
|
||||||
public bool ExtractChapterImagesDuringLibraryScan { get; set; }
|
public bool ExtractChapterImagesDuringLibraryScan { get; set; }
|
||||||
|
|
|
@ -779,6 +779,12 @@ namespace MediaBrowser.Model.Dto
|
||||||
/// <value>The timer identifier.</value>
|
/// <value>The timer identifier.</value>
|
||||||
public string TimerId { get; set; }
|
public string TimerId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the LUFS value.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The LUFS Value.</value>
|
||||||
|
public float LUFS { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the current program.
|
/// Gets or sets the current program.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
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;
|
||||||
|
@ -14,6 +17,7 @@ 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
|
||||||
|
@ -23,6 +27,10 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public 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;
|
||||||
|
@ -31,16 +39,19 @@ 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>
|
||||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||||
public AudioFileProber(
|
public AudioFileProber(
|
||||||
|
ILogger<AudioFileProber> logger,
|
||||||
IMediaSourceManager mediaSourceManager,
|
IMediaSourceManager mediaSourceManager,
|
||||||
IMediaEncoder mediaEncoder,
|
IMediaEncoder mediaEncoder,
|
||||||
IItemRepository itemRepo,
|
IItemRepository itemRepo,
|
||||||
ILibraryManager libraryManager)
|
ILibraryManager libraryManager)
|
||||||
{
|
{
|
||||||
|
_logger = logger;
|
||||||
_mediaEncoder = mediaEncoder;
|
_mediaEncoder = mediaEncoder;
|
||||||
_itemRepo = itemRepo;
|
_itemRepo = itemRepo;
|
||||||
_libraryManager = libraryManager;
|
_libraryManager = libraryManager;
|
||||||
|
@ -89,6 +100,54 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||||
Fetch(item, result, cancellationToken);
|
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;
|
return ItemUpdateType.MetadataImport;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -196,6 +255,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||||
audio.Album = tags.Album;
|
audio.Album = tags.Album;
|
||||||
audio.IndexNumber = Convert.ToInt32(tags.Track);
|
audio.IndexNumber = Convert.ToInt32(tags.Track);
|
||||||
audio.ParentIndexNumber = Convert.ToInt32(tags.Disc);
|
audio.ParentIndexNumber = Convert.ToInt32(tags.Disc);
|
||||||
|
|
||||||
if (tags.Year != 0)
|
if (tags.Year != 0)
|
||||||
{
|
{
|
||||||
var year = Convert.ToInt32(tags.Year);
|
var year = Convert.ToInt32(tags.Year);
|
||||||
|
|
|
@ -79,7 +79,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||||
NamingOptions namingOptions)
|
NamingOptions namingOptions)
|
||||||
{
|
{
|
||||||
_logger = loggerFactory.CreateLogger<ProbeProvider>();
|
_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);
|
_audioResolver = new AudioResolver(loggerFactory.CreateLogger<AudioResolver>(), localization, mediaEncoder, fileSystem, namingOptions);
|
||||||
_subtitleResolver = new SubtitleResolver(loggerFactory.CreateLogger<SubtitleResolver>(), localization, mediaEncoder, fileSystem, namingOptions);
|
_subtitleResolver = new SubtitleResolver(loggerFactory.CreateLogger<SubtitleResolver>(), localization, mediaEncoder, fileSystem, namingOptions);
|
||||||
_videoProber = new FFProbeVideoInfo(
|
_videoProber = new FFProbeVideoInfo(
|
||||||
|
|
Loading…
Reference in a new issue