diff --git a/config/default.yaml b/config/default.yaml index f3acfac1614..7edb0e98d68 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -635,6 +635,10 @@ transcoding: web_videos: enabled: false + # Apply dynamic loudness normalization when transcoding audio + audio_loudnorm: + enabled: false + # /!\ Requires ffmpeg >= 4.1 # Generate HLS playlists and fragmented MP4 files. Better playback than with Web Videos: # * Resolution change is smoother @@ -726,6 +730,10 @@ live: # Available in core PeerTube: 'default' profile: 'default' + # Apply dynamic loudness normalization when transcoding audio in live + audio_loudnorm: + enabled: false + resolutions: 0p: false # Audio only 144p: false diff --git a/packages/ffmpeg/src/ffmpeg-command-wrapper.ts b/packages/ffmpeg/src/ffmpeg-command-wrapper.ts index a2f464d68d6..863b403aa41 100644 --- a/packages/ffmpeg/src/ffmpeg-command-wrapper.ts +++ b/packages/ffmpeg/src/ffmpeg-command-wrapper.ts @@ -21,6 +21,8 @@ export interface FFmpegCommandWrapperOptions { logger: SimpleLogger lTags?: { tags: string[] } + audioLoudnorm?: boolean + updateJobProgress?: (progress?: number) => void onEnd?: () => void onError?: (err: Error) => void @@ -39,6 +41,8 @@ export class FFmpegCommandWrapper { private readonly logger: SimpleLogger private readonly lTags: { tags: string[] } + private readonly audioLoudnorm: boolean + private readonly updateJobProgress: (progress?: number) => void private readonly onEnd?: () => void private readonly onError?: (err: Error) => void @@ -54,6 +58,8 @@ export class FFmpegCommandWrapper { this.logger = options.logger this.lTags = options.lTags || { tags: [] } + this.audioLoudnorm = options.audioLoudnorm === true + this.updateJobProgress = options.updateJobProgress this.onEnd = options.onEnd @@ -72,6 +78,10 @@ export class FFmpegCommandWrapper { return this.command } + isAudioLoudnormEnabled () { + return this.audioLoudnorm === true + } + // --------------------------------------------------------------------------- debugLog (msg: string, meta: any = {}) { @@ -182,6 +192,11 @@ export class FFmpegCommandWrapper { const { streamType, videoType } = options + // Force re-encode audio if loudnorm is enabled + if (streamType === 'audio' && this.audioLoudnorm) { + options.canCopyAudio = false + } + const encodersToTry = this.availableEncoders.encodersToTry[videoType][streamType] const encoders = this.availableEncoders.available[videoType] @@ -210,6 +225,9 @@ export class FFmpegCommandWrapper { } } + // propagate audioLoudnorm flag to the profile builder + ;(options as any).audioLoudnorm = this.audioLoudnorm + const result = await builder( pick(options, [ 'input', @@ -220,7 +238,8 @@ export class FFmpegCommandWrapper { 'inputProbe', 'fps', 'inputRatio', - 'streamNum' + 'streamNum', + 'audioLoudnorm' ]) ) diff --git a/packages/ffmpeg/src/ffmpeg-default-transcoding-profile.ts b/packages/ffmpeg/src/ffmpeg-default-transcoding-profile.ts index 7e0743a2d50..1760c2a2cd9 100644 --- a/packages/ffmpeg/src/ffmpeg-default-transcoding-profile.ts +++ b/packages/ffmpeg/src/ffmpeg-default-transcoding-profile.ts @@ -11,6 +11,8 @@ import { import { EncoderOptionsBuilder, EncoderOptionsBuilderParams } from '@peertube/peertube-models' import { FfprobeData } from 'fluent-ffmpeg' +const LOUDNORM_FILTER = 'loudnorm=I=-14:TP=-1:LRA=11:linear=false' + const defaultX264VODOptionsBuilder: EncoderOptionsBuilder = (options: EncoderOptionsBuilderParams) => { const { fps, inputRatio, inputBitrate, resolution } = options @@ -40,7 +42,7 @@ const defaultX264LiveOptionsBuilder: EncoderOptionsBuilder = (options: EncoderOp } } -const defaultAACOptionsBuilder: EncoderOptionsBuilder = async ({ input, streamNum, canCopyAudio, inputProbe }) => { +const defaultAACOptionsBuilder: EncoderOptionsBuilder = async ({ input, streamNum, canCopyAudio, inputProbe, audioLoudnorm }) => { if (canCopyAudio && await canDoQuickAudioTranscode(input, inputProbe)) { return { copy: true, outputOptions: [] } } @@ -57,15 +59,29 @@ const defaultAACOptionsBuilder: EncoderOptionsBuilder = async ({ input, streamNu // Force stereo as it causes some issues with HLS playback in Chrome const base = [ '-channel_layout', 'stereo' ] + const opts = base.slice() + + if (audioLoudnorm) { + opts.push(buildStreamSuffix('-filter:a', streamNum), LOUDNORM_FILTER) + } + if (bitrate !== -1) { - return { outputOptions: base.concat([ buildStreamSuffix('-b:a', streamNum), bitrate + 'k' ]) } + return { outputOptions: opts.concat([ buildStreamSuffix('-b:a', streamNum), bitrate + 'k' ]) } } - return { outputOptions: base } + return { outputOptions: opts } } -const defaultLibFDKAACVODOptionsBuilder: EncoderOptionsBuilder = ({ streamNum }) => { - return { outputOptions: [ buildStreamSuffix('-q:a', streamNum), '5' ] } +const defaultLibFDKAACVODOptionsBuilder: EncoderOptionsBuilder = ({ streamNum, audioLoudnorm }) => { + const outputOptions: string[] = [] + + if (audioLoudnorm) { + outputOptions.push(buildStreamSuffix('-filter:a', streamNum), LOUDNORM_FILTER) + } + + outputOptions.push(buildStreamSuffix('-q:a', streamNum), '5') + + return { outputOptions } } export function getDefaultAvailableEncoders () { diff --git a/packages/ffmpeg/src/ffmpeg-vod.ts b/packages/ffmpeg/src/ffmpeg-vod.ts index a688bc04e69..de8b19b4fac 100644 --- a/packages/ffmpeg/src/ffmpeg-vod.ts +++ b/packages/ffmpeg/src/ffmpeg-vod.ts @@ -203,7 +203,7 @@ export class FFmpegVOD { const videoPath = this.getHLSVideoPath(options) - if (options.copyCodecs) { + if (options.copyCodecs && !this.commandWrapper.isAudioLoudnormEnabled()) { presetCopy(this.commandWrapper, { withAudio: !options.separatedAudio || !options.resolution, withVideo: !options.separatedAudio || !!options.resolution diff --git a/packages/models/src/server/custom-config.model.ts b/packages/models/src/server/custom-config.model.ts index 1d469664133..dce54455009 100644 --- a/packages/models/src/server/custom-config.model.ts +++ b/packages/models/src/server/custom-config.model.ts @@ -199,6 +199,10 @@ export interface CustomConfig { enabled: boolean splitAudioAndVideo: boolean } + + audioLoudnorm?: { + enabled: boolean + } } live: { @@ -228,6 +232,10 @@ export interface CustomConfig { fps: { max: number } + + audioLoudnorm?: { + enabled: boolean + } } } diff --git a/packages/models/src/videos/transcoding/video-transcoding.model.ts b/packages/models/src/videos/transcoding/video-transcoding.model.ts index ea929768140..e8a8469dabb 100644 --- a/packages/models/src/videos/transcoding/video-transcoding.model.ts +++ b/packages/models/src/videos/transcoding/video-transcoding.model.ts @@ -20,6 +20,9 @@ export type EncoderOptionsBuilderParams = { // For lives streamNum?: number + + // Flag to enforce audio loudness normalization + audioLoudnorm?: boolean } export type EncoderOptionsBuilder = (params: EncoderOptionsBuilderParams) => Promise | EncoderOptions diff --git a/server/core/controllers/api/config.ts b/server/core/controllers/api/config.ts index 0d8df44514c..c3beb1ce27a 100644 --- a/server/core/controllers/api/config.ts +++ b/server/core/controllers/api/config.ts @@ -422,6 +422,9 @@ function customConfig (): CustomConfig { originalFile: { keep: CONFIG.TRANSCODING.ORIGINAL_FILE.KEEP }, + audioLoudnorm: { + enabled: CONFIG.TRANSCODING.AUDIO_LOUDNORM.ENABLED + }, remoteRunners: { enabled: CONFIG.TRANSCODING.REMOTE_RUNNERS.ENABLED }, @@ -467,6 +470,9 @@ function customConfig (): CustomConfig { remoteRunners: { enabled: CONFIG.LIVE.TRANSCODING.REMOTE_RUNNERS.ENABLED }, + audioLoudnorm: { + enabled: CONFIG.LIVE.TRANSCODING.AUDIO_LOUDNORM.ENABLED + }, threads: CONFIG.LIVE.TRANSCODING.THREADS, profile: CONFIG.LIVE.TRANSCODING.PROFILE, resolutions: { diff --git a/server/core/helpers/ffmpeg/ffmpeg-options.ts b/server/core/helpers/ffmpeg/ffmpeg-options.ts index ec1b91f8697..68db55e2508 100644 --- a/server/core/helpers/ffmpeg/ffmpeg-options.ts +++ b/server/core/helpers/ffmpeg/ffmpeg-options.ts @@ -21,7 +21,12 @@ export function getFFmpegCommandWrapperOptions (type: CommandType, availableEnco warn: logger.warn.bind(logger), error: logger.error.bind(logger) }, - lTags: { tags: [ 'ffmpeg' ] } + lTags: { tags: [ 'ffmpeg' ] }, + audioLoudnorm: type === 'vod' + ? CONFIG.TRANSCODING.AUDIO_LOUDNORM.ENABLED + : type === 'live' + ? CONFIG.LIVE.TRANSCODING.AUDIO_LOUDNORM.ENABLED + : false } } diff --git a/server/core/initializers/config.ts b/server/core/initializers/config.ts index d691ac07b52..88753b0d72a 100644 --- a/server/core/initializers/config.ts +++ b/server/core/initializers/config.ts @@ -578,6 +578,12 @@ const CONFIG = { get PROFILE () { return config.get('transcoding.profile') }, + + AUDIO_LOUDNORM: { + get ENABLED () { + return config.get('transcoding.audio_loudnorm.enabled') + } + }, get ALWAYS_TRANSCODE_ORIGINAL_RESOLUTION () { return config.get('transcoding.always_transcode_original_resolution') }, @@ -706,6 +712,12 @@ const CONFIG = { return config.get('live.transcoding.profile') }, + AUDIO_LOUDNORM: { + get ENABLED () { + return config.get('live.transcoding.audio_loudnorm.enabled') + } + }, + get ALWAYS_TRANSCODE_ORIGINAL_RESOLUTION () { return config.get('live.transcoding.always_transcode_original_resolution') },