7

TL;DR an example of loudnorm 2pass in single line using ffpmpeg (ffmpeg-normalize is single thread/process and I can't combine other ffmpeg options to it)

Not really much examples on loudnorm since it's relatively new to ffmpeg, I've been using ffmpeg for 10 or so years. I am new to loudnorm however

I've read:

http://k.ylo.ph/2016/04/04/loudnorm.html

How can I normalize audio using ffmpeg?

which are helpful. however I'm trying to consolidate multiple ffmpeg entries into 1. using ffmpeg-normalize (the python) library limits you to a single thread/process as well. which is slow

for 2 pass of ffmpeg x264 you can simply do:

ffmpeg -y -i $FILE -c:v libx264 -b:v 4000k -pass 1 -c:a aac -b:a 256k -f mkv /dev/null && \
ffmpeg -i $FILE -c:v libx264 -b:v 4000k -pass 2 -c:a aac -b:a 256k $FILE.mkv

doesn't seem to exist for loudnorm though. I don't see a way to do it at all with a single and 2 pass?

currently I'm encoding video,then normalizing audio, and finally stripping metadata and chapter data from files if it exists (regardless if it exists or not)

this creates 3 throw-away files (including the original)

being able to do loudnorm in a single line would help me add other stuff to it also is it possible to do a 2pass of x264 and a 2pass of loudnorm simultaneously? as in have it process the two and then combine them in the second pass.

If possible I'd like examples of these and not links to things. I can google links on my own and have for several weeks. Thanks

1
  • The first pass of loudnorm is meant to survey the audio data and print out stream statistics. Some of those values have to be captured and fed to the filter in the 2nd pass. It cannot read them from a file (like in 2-pass encoding), so there isn't a counterpart to your encoding commands. You can combine first pass of x264 and loudnorm. Capture the loudnorm summary values, substitute them in 2nd pass command, which also does 2nd pass of x264. You don't need to write to a file in the first pass.
    – Gyan
    Apr 11, 2018 at 6:01

4 Answers 4

5

There's no way to run a two-pass loudnorm filter automatically with just ffmpeg, but you can use the ffmpeg-normalize program to do it for you. I know you've mentioned it, but if you want to encode video at the same time – particularly with two passes – then you will however have to work with one intermediate file, that is:

  • First run: ffmpeg-normalize on the original video, copying the original video stream.
  • Second run: x264-encoding (multithreaded) of the normalized audio file or the original file's video streams.

What you want to achieve simply cannot be done by ffmpeg alone. You need to program your own solution, especially if you want to handle several files in parallel. This would certainly speed up the process, even if a single ffmpeg run is only using one thread.

As a starting point, there's also a simpler Ruby script in the FFmpeg repository. It performs two loudnorm passes, reading the statistics of the first run. You may be able to modify it to additionaly run the two-pass x264 encoding with multithreading, that is, run the first x264 pass in the first run, the second in the second run:

First pass:

ffmpeg -y -i $FILE -c:v libx264 -b:v 4000k -pass 1 -filter:a loudnorm=print_format=json -f mkv /dev/null

Read the JSON statistics from the loudnorm output (e.g., using Python's JSON parser, or any other tool like grep or awk), then run the second pass:

ffmpeg -i $FILE -c:v libx264 -b:v 4000k -pass 2 -filter:a loudnorm=linear=true:measured_I=$input_i:measured_LRA=$input_lra:measured_tp=$input_tp:measured_thresh=$input_thresh -c:a aac -b:a 256k $FILE.mkv

Where $input_i, $input_lra, $input_tp, $input_thresh are the values read from the first pass.

3
  • Sorry but how could you decode JSON from first pass using Python? I tried with json.loads(pass1) but it doesn't work. Mar 10, 2022 at 18:00
  • @NineCattoRules You have to first filter out all irrelevant output. ffmpeg will print a lot of other things, so you need to grep for the correct line from loudnorm.
    – slhck
    Mar 12, 2022 at 18:09
  • How could I grep only loudnorm part? Could you please help me here ? Mar 16, 2022 at 14:15
5

There is no need for another program. This actually CAN be done with ffmpeg alone. I actually created a bat file that will do this.

cls
echo off
ffmpeg -i %1 -filter_complex "[0:a]loudnorm=I=-16:TP=-1.5:LRA=11:print_format=summary" -f null x 2>%1.txt
@for /f "tokens=3" %%a in ('findstr /C:"Input Integrated" %1.txt') do (set II=%%a)
echo %II% is the Input Integrated
@for /f "tokens=4" %%a in ('findstr /C:"Input True Peak" %1.txt') do (set ITP=%%a)
echo %ITP% is the Input True Peak
@for /f "tokens=3" %%a in ('findstr /C:"Input LRA" %1.txt') do (set ILRA=%%a)
echo %ILRA% is the Input LRA
@for /f "tokens=3" %%a in ('findstr /C:"Input Threshold" %1.txt') do (set IT=%%a)
echo %IT% is the Input Threshold
@for /f "tokens=3" %%a in ('findstr /C:"Output Integrated" %1.txt') do (set OI=%%a)
echo %OI% is the Output Integrated
@for /f "tokens=4" %%a in ('findstr /C:"Output True Peak" %1.txt') do (set OTP=%%a)
echo %OTP% is the Output True Peak
@for /f "tokens=3" %%a in ('findstr /C:"Output LRA" %1.txt') do (set OLRA=%%a)
echo %OLRA% is the Output LRA
@for /f "tokens=3" %%a in ('findstr /C:"Output Threshold" %1.txt') do (set OT=%%a)
echo %OT% is the Output Threshold
@for /f "tokens=3" %%a in ('findstr /C:"Target Offset" %1.txt') do (set TO=%%a)
echo %TO% is the Target Offset


ffmpeg -i %1 -af loudnorm=linear=true:I=-16:LRA=11:tp=-1.5:measured_I=%II%:measured_LRA=%ILRA%:measured_tp=%ITP%:measured_thresh=%IT%:offset=%TO%:print_format=summary loudnorm.wav

What this does is it first analyzes the audio and saves the results to a text file. It then reads the results from the text file and stores them as variables for each required measurement field. All you have to do it adjust the I= and TP= to your liking in the file.

0

Starting from petermg suggestion, this is the equivalent for *nix users:

ffmpeg -i $input -filter:a loudnorm=print_format=json -f null /dev/null 2>&1 >/dev/null | sed -n '/{/,/}/p' > /tmp/info

ii=`grep \"input_i\" /tmp/info | cut -d: -f2 | tr -cd [:digit:].-`
itp=`grep \"input_tp\" /tmp/info | cut -d: -f2 | tr -cd [:digit:].-`
ilra=`grep \"input_lra\" /tmp/info | cut -d: -f2 | tr -cd [:digit:].-`
it=`grep \"input_thresh\" /tmp/info | cut -d: -f2 | tr -cd [:digit:].-`
to=`grep \"target_offset\" /tmp/info | cut -d: -f2 | tr -cd [:digit:].-`

ffmpeg -i $input -af loudnorm=linear=true:I=-16:LRA=11:tp=-1.5:measured_I=$ii:measured_LRA=$ilra:measured_tp=$itp:measured_thresh=$it:offset=$to:print_format=summary $output

Replace $input and $output with your in and out files.

The first ffmpeg command produces a verbose output with a final JSON object, which is filtered out by the sed command in the pipe and stored in a temporary file. Each of the five commands that follow loads in a variable a relevant parameter to be fed to the second ffmpeg pass. The filtering operates in three steps of a pipe: first the line containing the relevant parameter is selected (grep), next the value is separated by the name (cut), and finally only the numeric part is sorted out (tr). The process is not 100% reliable since it depends on the output format of the ffmpeg command.

1
0

more bash scripts for the ffmpeg loudnorm filter

#! /usr/bin/env bash

# ffmpeg-loudnorm-analyze.sh

# analyze loudness of audio streams
# write result to stdout in json format
# example output: see end of script

# TODO verify ffmpeg output format: Parsed_loudnorm_([0-9]+)

set -e
set -u

input_file="$1"
shift
# extra args. example: -map 0:a:0 # process only the first audio stream
extra_args=("$@")

# https://ffmpeg.org/ffmpeg-filters.html#loudnorm

ffmpeg -i "$input_file" -pass 1 "${extra_args[@]}" \
  -filter:a loudnorm=print_format=json -f null -y /dev/null |
grep -E '^\[Parsed_loudnorm_([0-9]+) @ 0x[0-9a-f]+\]' -A12 |
perl -0777 -pe 's/}\s+\[Parsed_loudnorm_([0-9]+) @ 0x[0-9a-f]+\]\s+/    },\n    "$1": /g' |
perl -0777 -pe 's/\[Parsed_loudnorm_([0-9]+) @ 0x[0-9a-f]+\]\s+/    "$1": /g' |
sed 's/^}/    }/' |
sed '1 i\{'

printf '}\n';

exit

example output:

{
    "0": {
        "input_i": "-30.05",
        "input_tp": "-0.01",
        ...
    },
    "1": {
        "input_i": "-30.05",
        "input_tp": "-0.01",
        ...
    }
}
#! /usr/bin/env bash

# ffmpeg-loudnorm-format-filters.sh

# take a json file generated by ffmpeg-loudnorm-analyze.sh
# and format audio filters for ffmpeg

# example output:
# loudnorm=linear=true:I=-14:LRA=7:tp=-1:measured_I=-30.05:measured_LRA=16.60:measured_tp=-0.01:measured_thresh=-41.55:offset=0.46:print_format=summary

# to use the audio filter in ffmpeg:
# ffmpeg -af "loudnorm=..."

# https://ffmpeg.org/ffmpeg-filters.html#loudnorm

# https://superuser.com/questions/1604545/ffmpeg-loudnorm-results-in-not-very-loud
# https://youlean.co/loudness-standards-full-comparison-table/
max_integrated=-14
max_true_peak=-1
# LRA: loudness range target. Range is 1.0 - 50.0. Default value is 7.0
#loudnorm_LRA=11
loudnorm_LRA=7
loudnorm_linear=true
loudnorm_print_format=summary
loudnorm_extra_args=

set -e
set -u

json_file="$1"

stream_ids=$(jq -r 'keys[]' "$json_file" | sort -n)

s='"'
s+='loudnorm='
s+='linear='$loudnorm_linear':'
s+='I='$max_integrated':'
s+='LRA='$loudnorm_LRA':'
s+='tp='$max_true_peak':'
s+='measured_I=\(.input_i):'
s+='measured_LRA=\(.input_lra):'
s+='measured_tp=\(.input_tp):'
s+='measured_thresh=\(.input_thresh):'
s+='offset=\(.target_offset):'
s+='print_format='$loudnorm_print_format
s+="$loudnorm_extra_args"
s+='"'

jq_script="$s"

# print one filter per line, sorted by loudnorm result index
for stream_id in $stream_ids; do
  stream_json="$(jq -r ".\"$stream_id\"" "$json_file")"
  ffmpeg_loudnorm_filter="$(echo "$stream_json" | jq -r "$jq_script")"
  echo "$ffmpeg_loudnorm_filter"
done

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .