normalize.py 2.2 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
  1. #!/usr/bin/env python3
  2. import argparse
  3. import logging
  4. import shlex
  5. import subprocess
  6. HELP = '''
  7. Normalize audio input.
  8. The command uses ffprobe to analyze an input file with the ebur128
  9. filter, and finally run ffmpeg to normalize the input depending on the
  10. computed adjustment.
  11. ffmpeg encoding arguments can be passed through the extra arguments
  12. after options, for example as in:
  13. normalize.py --input input.mp3 --output output.mp3 -- -loglevel debug -y
  14. '''
  15. logging.basicConfig(format='normalize|%(levelname)s> %(message)s', level=logging.INFO)
  16. log = logging.getLogger()
  17. class Formatter(
  18. argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter
  19. ):
  20. pass
  21. def normalize():
  22. parser = argparse.ArgumentParser(description=HELP, formatter_class=Formatter)
  23. parser.add_argument('--input', '-i', required=True, help='specify input file')
  24. parser.add_argument('--output', '-o', required=True, help='specify output file')
  25. parser.add_argument('--dry-run', '-n', help='simulate commands', action='store_true')
  26. parser.add_argument('encode_arguments', nargs='*', help='specify encode options used for the actual encoding')
  27. args = parser.parse_args()
  28. analysis_cmd = [
  29. 'ffprobe', '-v', 'error', '-of', 'compact=p=0:nk=1',
  30. '-show_entries', 'frame_tags=lavfi.r128.I', '-f', 'lavfi',
  31. f"amovie='{args.input}',ebur128=metadata=1"
  32. ]
  33. def _run_command(cmd, dry_run=False):
  34. log.info(f"Running command:\n$ {shlex.join(cmd)}")
  35. if not dry_run:
  36. result = subprocess.run(cmd, check=True, stdout=subprocess.PIPE)
  37. return result
  38. result = _run_command(analysis_cmd)
  39. loudness = ref = -23
  40. for line in result.stdout.splitlines():
  41. sline = line.rstrip()
  42. if sline:
  43. loudness = sline
  44. adjust = ref - float(loudness)
  45. if abs(adjust) < 0.0001:
  46. logging.info(f"No normalization needed for '{args.input}'")
  47. return
  48. logging.info(f"Adjusting '{args.input}' by {adjust:.2f}dB...")
  49. normalize_cmd = [
  50. 'ffmpeg', '-i', args.input, '-af', f'volume={adjust:.2f}dB'
  51. ] + args.encode_arguments + [args.output]
  52. _run_command(normalize_cmd, args.dry_run)
  53. if __name__ == '__main__':
  54. normalize()