slhck / ffmpeg-normalize

Audio Normalization for Python/ffmpeg
MIT License
1.28k stars 118 forks source link

linear normalization with same configuration for input_LRA <1 and >7 #227

Closed mjhalwa closed 1 year ago

mjhalwa commented 1 year ago

Is your feature request related to a problem? Please describe.

Yes, I am currently using ffmpeg-normalize 1.26.4 in Python 3.10.9 with

from ffmpeg_normalize import FFmpegNormalize
normalizer = FFmpegNormalize(
        normalization_type="ebu",
        target_level=-23,
        print_stats=True,
        keep_loudness_range_target=True,  # needed for linear normalization
        true_peak=-2,
        dynamic=False,
        audio_codec=my_codec,
        audio_bitrate=my_bitrate,
        sample_rate=None,
        debug=True,
        progress=True,
    )
    normalizer.add_media_file(input_file, output_file)
    normalizer.run_normalization()

where 2nd runs with ffmpeg execute with lra={input_lra} which produces a linear normalized audio for any input_lra >= 1. If I try to convert a song with input_lra < 1 (e.g. 0.7 in audio from https://www.youtube.com/watch?v=HTYOa2_1aH4) I get the following error:

Value 0.700000 for parameter 'LRA' out of range [1 - 50]
Error setting option LRA to value 0.7.
Error initializing filter 'loudnorm' with args 'I=-23:TP=-2.0:LRA=0.7:measured_I=-12.7:measured_TP=-2.2:measured_LRA=0.7:measured_thresh=-22.7:offset=+0.1:linear=true:print_format=summary'
Error reinitializing filters!
Failed to inject frame into filter network: Result too large
Error while processing the decoded data for stream #0:0

I am able to resolve this issue by using loudness_range_target=7.0, instead of keep_loudness_range_target=True for any input_lra <= 7.0, but then get the following warning for input_lra > 7 (e.g. 16.10 in audio from https://www.youtube.com/watch?v=GibiNy4d4gc):

Input file had loudness range of 16.1. This is larger than the loudness range target (7.0). Normalization will revert to dynamic mode. Choose a higher target loudness range if you want linear normalization. Alternatively, use the --keep-loudness-range-target option to keep the target loudness range from the input.
In dynamic mode, the sample rate will automatically be set to 192 kHz by the loudnorm filter. Specify -ar/--sample-rate to override it.

which does perform dynamic normalization instead of linear normalization.

Describe the solution you'd like

In order to solve this and ensure backwards compatibility I'd suggest to a add a flag --keep-lra-above-loudness-range-target (constructor parameter: keep_lra_above_loudness_range_target=True). In combination with loudness_range_target=7.0. This should ensure LRA=7 for input_lra <= 7 and LRA=input_lra for input_lra > 7 and allows to convert any song with the same parameter-set independent of input_lra

Describe alternatives you've considered

An alternative solution would be to set input_lra < 1 to LRA=1 in order to fit into the range of [1 - 50] with something like --force-linear-normalization

Without any change in ffmpeg_normalize I would need to measure input_lra in order to know what parameters to set, but then I can basically use ffmpeg directly without using ffmpeg_normalize at all.

Additional context

I am new to the topic of audio normalization, followed some tutorials and weblinks, played around with ffmpeg in order to achieve linear normalization, as I did not like how dynamic normalization changed the audio. I read that target_LRA should be 7.0 (EBU R128) and therefore used 7.0 when possible (so if input_lra < 7) and used LRA=input_lra for LRA > 7. Using --keep-loudness-range-target in ffmpeg_normalize seems to override --loudness-range-target = 7.0 and therefore only use LRA=7.0, if the input_lra is already 7.0. So I hope --keep-lra-above-loudness-range-target might be an improvement by respecting --loudness-range-target = 7.0 as long as possible.

slhck commented 1 year ago

Thanks for this detailed description. I have to give it some thought … but wouldn't automatically setting measured_LRA to at least be 1 be simpler? Then users wouldn't have to specify extra flags. (I feel like we have a lot of flags already and they're getting quite confusing, even for me.)

slhck commented 1 year ago

Then again I do see your point. Would you like to file a PR for your new flag? I think if it's backwards compatible it's a good solution.

mjhalwa commented 1 year ago

I can give it a try - probably additional rounding up to 1 is a good solution to allow both flags to work for a broad range of input_LRAs.

Just one question: I am wondering if there is a reason for ffmpeg_normalize currently only supporting linear normalization with unchanged LRA in the second run? I mean, is it sufficient to set LRA=7 in the first run or is it preferrable to set LRA=7 in the 2nd run or is LRA=7 actually not required to fullfill EBU R128

slhck commented 1 year ago

I would probably just add a check before setting the opts here: https://github.com/slhck/ffmpeg-normalize/blob/7cd65d510dc2f2176358157980493c1735c62180/ffmpeg_normalize/_streams.py#L435 and output a warning if the loudness_range_target is below 1 (or above 50), saying that it was increased to 1 (or decreased to 50). That ensures we always produce a working command line. But I welcome a PR, since you also have the files to test with!

I believe that if you set LRA, you need to set it for the second run, since that will actually do the processing. The first run is only there to identify the properties of the file.

mjhalwa commented 1 year ago

I forked your project's master branch at 7cd65d5 and tried and setup (for me I use a virtual environment with pipenv):

pipenv install
pipenv install --dev -r .\requirements.dev.txt

I just wanted to make sure, I am able to run everything and don't miss anything. So I tried to execute help with with pipenv run python -m ffmpeg_normalize -h - that worked. But runnint pytest with

pipenv run pytest .\test\test.py

seems to fail in 5 out of 25 tests.

➜ pipenv run pytest .\test\test.py
================================================= test session starts =================================================
platform win32 -- Python 3.11.0, pytest-7.3.1, pluggy-1.0.0
rootdir: C:\Daten\Git\ffmpeg-normalize
collected 25 items

test\test.py ......FFF.............FF.                                                                           [100%]

====================================================== FAILURES =======================================================
____________________________________________ TestFFmpegNormalize.test_peak ____________________________________________
# ...
=============================================== short test summary info ===============================================
FAILED test/test.py::TestFFmpegNormalize::test_peak - AssertionError: assert False
FAILED test/test.py::TestFFmpegNormalize::test_rms - AssertionError: assert False
FAILED test/test.py::TestFFmpegNormalize::test_ebu - AssertionError: assert False
FAILED test/test.py::TestFFmpegNormalize::test_pre_filters - AssertionError: assert False
FAILED test/test.py::TestFFmpegNormalize::test_post_filters - AssertionError: assert False
============================================ 5 failed, 20 passed in 22.67s ============================================

Do I miss something or is this currently fine?

slhck commented 1 year ago

Could you please try to create a PR against this repository? I have automated tests that should ensure everything is fine.

mjhalwa commented 1 year ago

If you like, I could also create a PR to silently set LRA to 1, if it is lower than 1 for --keep-loudness-range-target, in order to fix this flag too. Or would you like me to add it to the same PR?

slhck commented 1 year ago

Fixed by c49480c3eeb64eeb02d021b3e9e574499f4cdc0f.

slhck commented 1 year ago

I added a constraint for the input LRA between 1 and 7 in 3187a02d6e5200949da36a8164277de6a6929b9a