m4b-tool icon indicating copy to clipboard operation
m4b-tool copied to clipboard

Feature request: loudness normalization / volume change

Open vishhvaan opened this issue 3 years ago • 10 comments

Thank you for making this great tool. I have source files, mp3 files divided by chapters which I want to merge. These files are a little quiet and I want to increase the volume of the final file.

I am using the --ffmpeg-param to pass --ffmpeg-param="-filter:a" --ffmpeg-param='"volume=5"'. But when run in -vvv mode, I see ffmpeg report "Trailing options were found on the commandline." The resulting file is not louder than if the ffmpeg params were not run. I am assuming these options are ignored. Can I pass options prior to the ffmpeg output? Or is there some other way of doing this. I eventually want to normalize volume with ffmpeg's loudnorm.

I realize I can write a script to iterate through the source files to increase volumes prior to merging. But I would rather not transcode the source files twice (once for volume and once when creating the m4b). Plus if I can do this through m4b-tool, it would save me a step.

vishhvaan avatar Feb 01 '21 02:02 vishhvaan

Hey @vishhvaan , thanks for reporting this. Well, although ffmpeg is a great tool (it really is!!), it has its caveats. One of these is that the order of parameters is important, which leads to this issue. If I would change the position of the parameter insertion (e.g. before output), this would cause other ffmpeg-params to be ignored or work different (e.g. threads).

To answer your questions:

Can I pass options prior to the ffmpeg output? Or is there some other way of doing this. I eventually want to normalize volume with ffmpeg's loudnorm.

No, unfortunately, there is no way I am aware of. Since m4b-tool is just a tool to simplify usage of audio specific command line options in ffmpeg, a feature like --ffmpeg-prepend-param to manually add something to ffmpeg would not make too much sense, but loudness normalization / volume change is a specific feature request, that I would love to see implemented (of course also by myself ;-)).

So if you don't mind, I'll change this issue to a feature request:

loudness normalization / volume change

Tasks:

  • [ ] Add a parameter for volume change: --change-volume
    • ffmpeg -i input.wav -filter:a "volume=0.5" output.wav
  • [ ] Evaluate --normalize-volume-twopass because it is recommended by ffmpeg
    • This is recommended for most applications, as it will lead to a more uniform loudness level compared to simple peak-based normalization. However, it is recommended to run the normalization with two passes, extracting the measured values from the first run, then using the values in a second run with linear normalization enabled. See the loudnorm filter documentation for more.

  • [ ] Add parmeter for loudness normalisation based on the research of second task: --normalize-volume
    • ffmpeg -i input.wav -filter:a loudnorm output.wav

ffmpeg resources and examples:

  • http://ffmpeg.org/ffmpeg-filters.html#volume
  • https://trac.ffmpeg.org/wiki/AudioVolume
  • https://superuser.com/questions/323119/how-can-i-normalize-audio-using-ffmpeg

sandreas avatar Feb 01 '21 08:02 sandreas

This would be great. Thank you!

vishhvaan avatar Feb 01 '21 14:02 vishhvaan

According to the article http://k.ylo.ph/2016/04/04/loudnorm.html the following procedure would be the most accurate:

first pass - measuring volume

For measuring the volume correctly, three target values are required:

  • LUFS_IL value (e.g. -16)
  • dBTP value (e.g. -1.5)
  • LRA value (e.g. -11)
ffmpeg -hide_banner -i input.m4n -af loudnorm=I={LUFS_IL}:TP={dBTP}:LRA={LRA}:print_format=json -f null -

To validate useful settings, the following values from https://github.com/peterforgacs/ffmpeg-normalize/blob/master/src/normalizer.ts#L94 might be a good start:

input_i = base: -23, min: -70.0,max: -5.0
input_lra = base: 7.0, min: 1.0, max: 20.0
input_tp = base: -2.0, min: -9.0, max: 0.0

second pass - adjusting value by measurements

The output from above can be used to normalize loudness in a second pass:

Last 12 lines of the result:

{
	"input_i" : "-12.97",
	"input_tp" : "-0.71",
	"input_lra" : "6.80",
	"input_thresh" : "-23.81",
	"output_i" : "-16.97",
	"output_tp" : "-4.73",
	"output_lra" : "5.80",
	"output_thresh" : "-27.79",
	"normalization_type" : "dynamic",
	"target_offset" : "0.97"
}

command to normalize loudness:

ffmpeg -i input.m4a -af loudnorm=I={LUFS_IL}:TP={dBTP}:LRA={LRA}:measured_I={input_i}:measured_TP={input_tp}:measured_LRA={input_lra}:measured_thresh={input_thresh}:offset={target_offset}:linear=true:print_format=summary output.m4a

According to the number of --audio-channels, the option dual_mono=true will be added:

ffmpeg -i input.m4a -af loudnorm=I={LUFS_IL}:TP={dBTP}:LRA={LRA}:measured_I={input_i}:measured_TP={input_tp}:measured_LRA={input_lra}:measured_thresh={input_thresh}:offset={target_offset}:linear=true:dual_mono=true:print_format=summary output.m4a

So the way this will be implemented is:

--normalize-volume // boolean, use default options of -16,-1.5,11
--normalize-volume-options=-16,-1.5,11 // LUFS_IL,dBTP,LRA (default)
--normalize-volume-options=0.5 // change volume level with fixed value without two pass measuring

sandreas avatar Feb 01 '21 16:02 sandreas

This is a really nice implementation. 2-pass, reading from the JSON, seems to the norm. FFmpeg themselves do something similar.

If users are going to normalize, they can increase/decrease the volume of the final product by adjusting the LUFS_IL,dBTP,LRA parameters. If you aren't going to use the --normalize-volume-options parameter with the normalization, maybe it would be less confusing to rename it to --adjust-audio-volume=1 (which can be 1.5 or 5dB) as described here. I haven't seen an example of this option used together with the normalization. Maybe users will have to be warned if they use it together with a true --normalize-volume boolean? A more complicated but more intuitive (to me) solution would be to adjust LUFS_IL based on --adjust-audio-volume input. Not sure if anyone has done this before or if it is feasible.

However you decide to name it, thank you for looking into this.

vishhvaan avatar Feb 01 '21 17:02 vishhvaan

ffmpeg-normalize seems to have a way of bringing the audio up to a specified target: https://github.com/slhck/ffmpeg-normalize#normalization

vishhvaan avatar Feb 01 '21 18:02 vishhvaan

Hello Andreas - I'll be happy to write the code myself. If you can point me in the right direction, I can see what I can do and put in a pull request. Message me to talk about this.

vishhvaan avatar Feb 14 '21 17:02 vishhvaan

Well, since this is most useful on merge, i would place the methods for loudness in here: https://github.com/sandreas/m4b-tool/blob/2ac2631d68341ae965aaef5a1745eec95c45288d/src/library/Executables/Ffmpeg.php#L770

public function normalizeVolume(SplFileInfo $file, $options) {
// ...
}

public function adjustVolume(SplFileInfo $file, $options) {
// ...
}

Options from command line arguments can be placed here:

  • https://github.com/sandreas/m4b-tool/blob/2ac2631d68341ae965aaef5a1745eec95c45288d/src/library/Command/MergeCommand.php#L54
  • https://github.com/sandreas/m4b-tool/blob/2ac2631d68341ae965aaef5a1745eec95c45288d/src/library/Command/MergeCommand.php#L142

Then usage of options and execution here:

https://github.com/sandreas/m4b-tool/blob/2ac2631d68341ae965aaef5a1745eec95c45288d/src/library/Command/MergeCommand.php#L869

        // this is new
        if($options) {
             $this->ffmpeg->ajdustVolume($outputTmpFile, $options);
             // or
             $this->ffmpeg->normalizeVolume($outputTmpFile, $options);
        }
        $this->tagFile($outputTmpFile, $tag, $flags);

Something like this.

sandreas avatar Feb 14 '21 18:02 sandreas

Great. I'll take a look :+1:

vishhvaan avatar Feb 14 '21 18:02 vishhvaan

Has there been any progress on your side?

sandreas avatar Jun 02 '21 17:06 sandreas

@sandreas Loudness correction should definitely be done during the conversion, before merge:

  1. Doing it during merge prevents us from using the option -c copy. In fact using a filter forces a re-encode, which degrades the audio quality (double re-encoding).
  2. If you apply filters such as fade-in/fade-out in the conversion step, having the loudness normalization at merge will ruin them. Instead the loudness normalization filter should be before the fade-in/fade-in filters in the filters pipeline. See here for a use case of fade-in/fade-out filters: https://github.com/sandreas/m4b-tool/issues/225

Note that you'll need to calculate the loudness normalization settings on the concatenation of all the input files beforehand, and then apply the correction individually on each file during the conversion step (using the calculated loudness normalization over the concatenation of all inputs).

See https://github.com/sandreas/m4b-tool/issues/226

What do you think?

Below a copy of my approach:


Particularly useful for voices as they can get farther and closer to the mic.

  • Explanations: http://k.ylo.ph/2016/04/04/loudnorm.html
  • loudnorm documentation: https://ffmpeg.org/ffmpeg-filters.html#loudnorm

Note: the first loudnorm pass should be run through the collection of all the inputs to make sure that the dynamic normalization is consistent across the whole merged file.

For example for converting MP3 files to M4B:

ffmpeg -i part1.mp3 -i part2.mp3 -i part3.mp3 -filter_complex 'concat=n=3:v=0:a=1[a]; [a]loudnorm=I=-16:TP=-1.5:LRA=11:print_format=json' -f null -

output:

{
	"input_i" : "-22.99",
	"input_tp" : "-6.99",
	"input_lra" : "7.00",
	"input_thresh" : "-34.56",
	"output_i" : "-16.73",
	"output_tp" : "-1.50",
	"output_lra" : "6.80",
	"output_thresh" : "-28.29",
	"normalization_type" : "dynamic",
	"target_offset" : "0.73"
}
ffmpeg -i part1.mp3 -ab 196k -ar 44100 -ac 2 -af loudnorm=I=-16:TP=-1.5:LRA=11:measured_I=-22.99:measured_TP=-6.99:measured_LRA=7:measured_thresh=-34.56:offset=0.73:linear=true:print_format=summary -f mp4 part1.m4b

ffmpeg -i part2.mp3 -ab 196k -ar 44100 -ac 2 -af loudnorm=I=-16:TP=-1.5:LRA=11:measured_I=-22.99:measured_TP=-6.99:measured_LRA=7:measured_thresh=-34.56:offset=0.73:linear=true:print_format=summary -f mp4 part2.m4b

ffmpeg -i part3.mp3 -ab 196k -ar 44100 -ac 2 -af loudnorm=I=-16:TP=-1.5:LRA=11:measured_I=-22.99:measured_TP=-6.99:measured_LRA=7:measured_thresh=-34.56:offset=0.73:linear=true:print_format=summary -f mp4 part3.m4b

m4b-tool merge -vvv --debug --no-conversion --include-extensions=m4b --output-file="merged.m4b" .

devnoname120 avatar Feb 22 '23 13:02 devnoname120