aboutsummaryrefslogtreecommitdiffstats
path: root/tools/normalize.py
blob: b36ff74ad044c8e4a10727974715a474ed4ca688 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#!/usr/bin/env python3

import argparse
import logging
import shlex
import subprocess

HELP = '''
Normalize audio input.

The command uses ffprobe to analyze an input file with the ebur128
filter, and finally run ffmpeg to normalize the input depending on the
computed adjustment.

ffmpeg encoding arguments can be passed through the extra arguments
after options, for example as in:
normalize.py --input input.mp3 --output output.mp3 -- -loglevel debug -y
'''

logging.basicConfig(format='normalize|%(levelname)s> %(message)s', level=logging.INFO)
log = logging.getLogger()


class Formatter(
    argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter
):
    pass


def normalize():
    parser = argparse.ArgumentParser(description=HELP, formatter_class=Formatter)
    parser.add_argument('--input', '-i', required=True, help='specify input file')
    parser.add_argument('--output', '-o', required=True, help='specify output file')
    parser.add_argument('--dry-run', '-n', help='simulate commands', action='store_true')
    parser.add_argument('encode_arguments', nargs='*', help='specify encode options used for the actual encoding')

    args = parser.parse_args()

    analysis_cmd = [
        'ffprobe', '-v', 'error', '-of', 'compact=p=0:nk=1',
        '-show_entries', 'frame_tags=lavfi.r128.I', '-f', 'lavfi',
        f"amovie='{args.input}',ebur128=metadata=1"
    ]

    def _run_command(cmd, dry_run=False):
        log.info(f"Running command:\n$ {shlex.join(cmd)}")
        if not dry_run:
            result = subprocess.run(cmd, check=True, stdout=subprocess.PIPE)
            return result

    result = _run_command(analysis_cmd)

    loudness = ref = -23
    for line in result.stdout.splitlines():
        sline = line.rstrip()
        if sline:
            loudness = sline

    adjust = ref - float(loudness)
    if abs(adjust) < 0.0001:
        logging.info(f"No normalization needed for '{args.input}'")
        return

    logging.info(f"Adjusting '{args.input}' by {adjust:.2f}dB...")
    normalize_cmd = [
        'ffmpeg', '-i', args.input, '-af', f'volume={adjust:.2f}dB'
    ] + args.encode_arguments + [args.output]

    _run_command(normalize_cmd, args.dry_run)


if __name__ == '__main__':
    normalize()