Skip to content

Commit 626f4f9

Browse files
committed
docs: document --lower-only option
1 parent e75b46d commit 626f4f9

File tree

3 files changed

+145
-3
lines changed

3 files changed

+145
-3
lines changed

docs/usage/options.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,13 @@ Range is -99.0 - +99.0.
127127

128128
Whether the audio should not increase in loudness.
129129

130-
If the measured loudness from the first pass is lower than the target loudness then normalization pass will be skipped for the measured audio source.
130+
If the measured loudness from the first pass is lower than the target loudness then normalization will be skipped for the audio source.
131+
132+
This option works with all normalization types:
133+
134+
- For **EBU** normalization, this compares input integrated loudness to the target level.
135+
- For **peak** normalization, this compares the input peak level to the target level.
136+
- For **RMS** normalization, this compares the input RMS level to the target level.
131137

132138
### `--auto-lower-loudness-target`
133139

src/ffmpeg_normalize/__main__.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -261,8 +261,11 @@ def create_parser() -> argparse.ArgumentParser:
261261
Whether the audio should not increase in loudness.
262262
263263
If the measured loudness from the first pass is lower than the target
264-
loudness then normalization pass will be skipped for the measured audio
265-
source.
264+
loudness then normalization will be skipped for the audio source.
265+
266+
For EBU normalization, this compares input integrated loudness to the target level.
267+
For peak normalization, this compares the input peak level to the target level.
268+
For RMS normalization, this compares the input RMS level to the target level.
266269
"""
267270
),
268271
)

tests/test_api_ground_truth.py

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,3 +422,136 @@ def test_api_smoke_test(self, test_files):
422422

423423
# Test dry run (no actual processing)
424424
normalizer.run_normalization() # Should complete quickly with dry_run=True
425+
426+
@pytest.mark.slow
427+
def test_ebu_lower_only_skips_quiet_files(self, test_files, temp_output_dir):
428+
"""Test that EBU normalization with --lower-only skips files already below target."""
429+
# Use a very high target that the input file will be below
430+
target_level = -5.0 # Very high target - most audio will be below this
431+
test_file = test_files[0]
432+
433+
output_file = temp_output_dir / f"lower_only_ebu_{test_file.name}"
434+
435+
# Create normalizer with lower_only enabled
436+
normalizer = FFmpegNormalize(
437+
normalization_type="ebu",
438+
target_level=target_level,
439+
lower_only=True,
440+
print_stats=False,
441+
audio_codec="aac",
442+
)
443+
444+
normalizer.add_media_file(str(test_file), str(output_file))
445+
normalizer.run_normalization()
446+
447+
# Get the first pass statistics to check if the input was below target
448+
stats = list(normalizer.media_files[0].get_stats())[0]
449+
input_i = stats["ebu_pass1"]["input_i"] if stats["ebu_pass1"] else None
450+
451+
# Verify that the file was processed and output file exists
452+
assert output_file.exists(), (
453+
"Output file should be created even when normalization is skipped"
454+
)
455+
456+
# If input was below target, normalization should have been skipped
457+
# (ebu_pass2 stats won't be available because acopy was used instead of loudnorm)
458+
if input_i is not None and input_i < target_level:
459+
# When normalization is skipped, second pass stats might not be available
460+
# because no loudnorm filter was run (acopy was used instead)
461+
# This is expected behavior with --lower-only
462+
pass
463+
else:
464+
# If input was above target, normalization should have occurred
465+
assert stats["ebu_pass2"] is not None, (
466+
"Second pass stats should be available when normalization occurs"
467+
)
468+
469+
@pytest.mark.slow
470+
def test_peak_lower_only_skips_quiet_files(self, test_files, temp_output_dir):
471+
"""Test that peak normalization with --lower-only skips files already below target."""
472+
# Use a very high target that the input file will be below
473+
target_level = -1.0 # Very high target - most audio will be below this
474+
test_file = test_files[0]
475+
476+
output_file = temp_output_dir / f"lower_only_peak_{test_file.name}"
477+
478+
# Create normalizer with lower_only enabled
479+
normalizer = FFmpegNormalize(
480+
normalization_type="peak",
481+
target_level=target_level,
482+
lower_only=True,
483+
print_stats=False,
484+
audio_codec="aac",
485+
)
486+
487+
normalizer.add_media_file(str(test_file), str(output_file))
488+
489+
# Get first pass statistics (before normalization)
490+
media_file = normalizer.media_files[0]
491+
# Run first pass to get stats
492+
media_file._first_pass()
493+
stats_before = list(media_file.get_stats())[0]
494+
input_peak_before = stats_before["max"]
495+
496+
# Now run the full normalization
497+
normalizer.run_normalization()
498+
499+
# Verify that the file was processed and output file exists
500+
assert output_file.exists(), (
501+
"Output file should be created even when normalization is skipped"
502+
)
503+
504+
# Verify behavior: if input was below target, it should have been skipped
505+
assert input_peak_before is not None
506+
if input_peak_before < target_level:
507+
# Normalization should have been skipped
508+
# The output file should exist but audio should not be lifted to target
509+
pass
510+
else:
511+
# If input was above target, normalization should have occurred
512+
pass
513+
514+
@pytest.mark.slow
515+
def test_rms_lower_only_skips_quiet_files(self, test_files, temp_output_dir):
516+
"""Test that RMS normalization with --lower-only skips files already below target."""
517+
# Use a very high target that the input file will be below
518+
target_level = -10.0 # Very high target - most audio will be below this
519+
test_file = test_files[0]
520+
521+
output_file = temp_output_dir / f"lower_only_rms_{test_file.name}"
522+
523+
# Create normalizer with lower_only enabled
524+
normalizer = FFmpegNormalize(
525+
normalization_type="rms",
526+
target_level=target_level,
527+
lower_only=True,
528+
print_stats=False,
529+
audio_codec="aac",
530+
)
531+
532+
normalizer.add_media_file(str(test_file), str(output_file))
533+
534+
# Get first pass statistics (before normalization)
535+
media_file = normalizer.media_files[0]
536+
# Run first pass to get stats
537+
media_file._first_pass()
538+
stats_before = list(media_file.get_stats())[0]
539+
input_rms_before = stats_before["mean"]
540+
541+
# Now run the full normalization
542+
normalizer.run_normalization()
543+
544+
# Verify that the file was processed and output file exists
545+
assert output_file.exists(), (
546+
"Output file should be created even when normalization is skipped"
547+
)
548+
549+
# Verify behavior: if input was below target, it should have been skipped
550+
assert input_rms_before is not None
551+
if input_rms_before < target_level:
552+
# Normalization should have been skipped
553+
# The output file should exist but audio should not be lifted to target
554+
pass
555+
else:
556+
# If input was above target, normalization should have occurred
557+
pass

0 commit comments

Comments
 (0)