Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
| import argparse | |
| import textwrap | |
| import logging | |
| import os | |
| import shlex | |
| import json | |
| try: | |
| from json.decoder import JSONDecodeError | |
| except ImportError: | |
| JSONDecodeError = ValueError | |
| from ._version import __version__ | |
| from ._ffmpeg_normalize import FFmpegNormalize, NORMALIZATION_TYPES | |
| from ._errors import FFmpegNormalizeError | |
| from ._logger import setup_custom_logger | |
| logger = setup_custom_logger("ffmpeg_normalize") | |
| def create_parser(): | |
| parser = argparse.ArgumentParser( | |
| prog="ffmpeg-normalize", | |
| description=textwrap.dedent( | |
| """\ | |
| ffmpeg-normalize v{} -- command line tool for normalizing audio files | |
| """.format( | |
| __version__ | |
| ) | |
| ), | |
| usage=textwrap.dedent( | |
| """\ | |
| ffmpeg-normalize input [input ...] | |
| [-h] | |
| [-o OUTPUT [OUTPUT ...]] [-of OUTPUT_FOLDER] | |
| [-f] [-d] [-v] [-q] [-n] [-pr] | |
| [--version] | |
| [-nt {ebu,rms,peak}] [-t TARGET_LEVEL] [-p] | |
| [-lrt LOUDNESS_RANGE_TARGET] [-tp TRUE_PEAK] [--offset OFFSET] [--dual-mono] | |
| [-c:a AUDIO_CODEC] [-b:a AUDIO_BITRATE] [-ar SAMPLE_RATE] [-koa] | |
| [-prf PRE_FILTER] [-pof POST_FILTER] | |
| [-vn] [-c:v VIDEO_CODEC] | |
| [-sn] [-mn] [-cn] | |
| [-ei EXTRA_INPUT_OPTIONS] [-e EXTRA_OUTPUT_OPTIONS] | |
| [-ofmt OUTPUT_FORMAT] | |
| [-ext EXTENSION] | |
| """ | |
| ), | |
| formatter_class=argparse.RawTextHelpFormatter, | |
| epilog=textwrap.dedent( | |
| """\ | |
| The program additionally respects environment variables: | |
| - `TMP` / `TEMP` / `TMPDIR` | |
| Sets the path to the temporary directory in which files are | |
| stored before being moved to the final output directory. | |
| Note: You need to use full paths. | |
| - `FFMPEG_PATH` | |
| Sets the full path to an `ffmpeg` executable other than | |
| the system default. | |
| Author: Werner Robitza | |
| License: MIT | |
| Homepage / Issues: https://github.com/slhck/ffmpeg-normalize | |
| """ | |
| ), | |
| ) | |
| group_io = parser.add_argument_group("File Input/output") | |
| group_io.add_argument("input", nargs="+", help="Input media file(s)") | |
| group_io.add_argument( | |
| "-o", | |
| "--output", | |
| nargs="+", | |
| help=textwrap.dedent( | |
| """\ | |
| Output file names. Will be applied per input file. | |
| If no output file name is specified for an input file, the output files | |
| will be written to the default output folder with the name `<input>.<ext>`, | |
| where `<ext>` is the output extension (see `-ext` option). | |
| Example: ffmpeg-normalize 1.wav 2.wav -o 1n.wav 2n.wav | |
| """ | |
| ), | |
| ) | |
| group_io.add_argument( | |
| "-of", | |
| "--output-folder", | |
| type=str, | |
| help=textwrap.dedent( | |
| """\ | |
| Output folder (default: `normalized`) | |
| This folder will be used for input files that have no explicit output | |
| name specified. | |
| """ | |
| ), | |
| default="normalized", | |
| ) | |
| group_general = parser.add_argument_group("General Options") | |
| group_general.add_argument( | |
| "-f", "--force", action="store_true", help="Force overwrite existing files" | |
| ) | |
| group_general.add_argument( | |
| "-d", "--debug", action="store_true", help="Print debugging output" | |
| ) | |
| group_general.add_argument( | |
| "-v", "--verbose", action="store_true", help="Print verbose output" | |
| ) | |
| group_general.add_argument( | |
| "-q", "--quiet", action="store_true", help="Only print errors in output" | |
| ) | |
| group_general.add_argument( | |
| "-n", | |
| "--dry-run", | |
| action="store_true", | |
| help="Do not run normalization, only print what would be done", | |
| ) | |
| group_general.add_argument( | |
| "-pr", | |
| "--progress", | |
| action="store_true", | |
| help="Show progress bar for files and streams", | |
| ) | |
| group_general.add_argument( | |
| "--version", | |
| action="version", | |
| version=f"%(prog)s v{__version__}", | |
| help="Print version and exit", | |
| ) | |
| group_normalization = parser.add_argument_group("Normalization") | |
| group_normalization.add_argument( | |
| "-nt", | |
| "--normalization-type", | |
| type=str, | |
| choices=NORMALIZATION_TYPES, | |
| help=textwrap.dedent( | |
| """\ | |
| Normalization type (default: `ebu`). | |
| EBU normalization performs two passes and normalizes according to EBU | |
| R128. | |
| RMS-based normalization brings the input file to the specified RMS | |
| level. | |
| Peak normalization brings the signal to the specified peak level. | |
| """ | |
| ), | |
| default="ebu", | |
| ) | |
| group_normalization.add_argument( | |
| "-t", | |
| "--target-level", | |
| type=float, | |
| help=textwrap.dedent( | |
| """\ | |
| Normalization target level in dB/LUFS (default: -23). | |
| For EBU normalization, it corresponds to Integrated Loudness Target | |
| in LUFS. The range is -70.0 - -5.0. | |
| Otherwise, the range is -99 to 0. | |
| """ | |
| ), | |
| default=-23.0, | |
| ) | |
| group_normalization.add_argument( | |
| "-p", | |
| "--print-stats", | |
| action="store_true", | |
| help="Print first pass loudness statistics formatted as JSON to stdout", | |
| ) | |
| # group_normalization.add_argument( | |
| # '--threshold', | |
| # type=float, | |
| # help=textwrap.dedent("""\ | |
| # Threshold below which normalization should not be run. | |
| # If the stream falls within the threshold, it will simply be copied. | |
| # """), | |
| # default=0.5 | |
| # ) | |
| group_ebu = parser.add_argument_group("EBU R128 Normalization") | |
| group_ebu.add_argument( | |
| "-lrt", | |
| "--loudness-range-target", | |
| type=float, | |
| help=textwrap.dedent( | |
| """\ | |
| EBU Loudness Range Target in LUFS (default: 7.0). | |
| Range is 1.0 - 20.0. | |
| """ | |
| ), | |
| default=7.0, | |
| ) | |
| group_ebu.add_argument( | |
| "-tp", | |
| "--true-peak", | |
| type=float, | |
| help=textwrap.dedent( | |
| """\ | |
| EBU Maximum True Peak in dBTP (default: -2.0). | |
| Range is -9.0 - +0.0. | |
| """ | |
| ), | |
| default=-2.0, | |
| ) | |
| group_ebu.add_argument( | |
| "--offset", | |
| type=float, | |
| help=textwrap.dedent( | |
| """\ | |
| EBU Offset Gain (default: 0.0). | |
| The gain is applied before the true-peak limiter in the first pass only. | |
| The offset for the second pass will be automatically determined based on the first pass statistics. | |
| Range is -99.0 - +99.0. | |
| """ | |
| ), | |
| default=0.0, | |
| ) | |
| group_ebu.add_argument( | |
| "--dual-mono", | |
| action="store_true", | |
| help=textwrap.dedent( | |
| """\ | |
| Treat mono input files as "dual-mono". | |
| If a mono file is intended for playback on a stereo system, its EBU R128 | |
| measurement will be perceptually incorrect. If set, this option will | |
| compensate for this effect. Multi-channel input files are not affected | |
| by this option. | |
| """ | |
| ), | |
| ) | |
| group_acodec = parser.add_argument_group("Audio Encoding") | |
| group_acodec.add_argument( | |
| "-c:a", | |
| "--audio-codec", | |
| type=str, | |
| help=textwrap.dedent( | |
| """\ | |
| Audio codec to use for output files. | |
| See `ffmpeg -encoders` for a list. | |
| Will use PCM audio with input stream bit depth by default. | |
| """ | |
| ), | |
| ) | |
| group_acodec.add_argument( | |
| "-b:a", | |
| "--audio-bitrate", | |
| type=str, | |
| help=textwrap.dedent( | |
| """\ | |
| Audio bitrate in bits/s, or with K suffix. | |
| If not specified, will use codec default. | |
| """ | |
| ), | |
| ) | |
| group_acodec.add_argument( | |
| "-ar", | |
| "--sample-rate", | |
| type=str, | |
| help=textwrap.dedent( | |
| """\ | |
| Audio sample rate to use for output files in Hz. | |
| Will use input sample rate by default, except for EBU normalization, | |
| which will change the input sample rate to 192 kHz. | |
| """ | |
| ), | |
| ) | |
| group_acodec.add_argument( | |
| "-koa", | |
| "--keep-original-audio", | |
| action="store_true", | |
| help="Copy original, non-normalized audio streams to output file", | |
| ) | |
| group_acodec.add_argument( | |
| "-prf", | |
| "--pre-filter", | |
| type=str, | |
| help=textwrap.dedent( | |
| """\ | |
| Add an audio filter chain before applying normalization. | |
| Multiple filters can be specified by comma-separating them. | |
| """ | |
| ), | |
| ) | |
| group_acodec.add_argument( | |
| "-pof", | |
| "--post-filter", | |
| type=str, | |
| help=textwrap.dedent( | |
| """\ | |
| Add an audio filter chain after applying normalization. | |
| Multiple filters can be specified by comma-separating them. | |
| For EBU, the filter will be applied during the second pass. | |
| """ | |
| ), | |
| ) | |
| group_vcodec = parser.add_argument_group("Other Encoding Options") | |
| group_vcodec.add_argument( | |
| "-vn", | |
| "--video-disable", | |
| action="store_true", | |
| help="Do not write video streams to output", | |
| ) | |
| group_vcodec.add_argument( | |
| "-c:v", | |
| "--video-codec", | |
| type=str, | |
| help=textwrap.dedent( | |
| """\ | |
| Video codec to use for output files (default: 'copy'). | |
| See `ffmpeg -encoders` for a list. | |
| Will attempt to copy video codec by default. | |
| """ | |
| ), | |
| default="copy", | |
| ) | |
| group_vcodec.add_argument( | |
| "-sn", | |
| "--subtitle-disable", | |
| action="store_true", | |
| help="Do not write subtitle streams to output", | |
| ) | |
| group_vcodec.add_argument( | |
| "-mn", | |
| "--metadata-disable", | |
| action="store_true", | |
| help="Do not write metadata to output", | |
| ) | |
| group_vcodec.add_argument( | |
| "-cn", | |
| "--chapters-disable", | |
| action="store_true", | |
| help="Do not write chapters to output", | |
| ) | |
| group_format = parser.add_argument_group("Input/Output options") | |
| group_format.add_argument( | |
| "-ei", | |
| "--extra-input-options", | |
| type=str, | |
| help=textwrap.dedent( | |
| """\ | |
| Extra input options list. | |
| A list of extra ffmpeg command line arguments valid for the input, | |
| applied before ffmpeg's `-i`. | |
| You can either use a JSON-formatted list (i.e., a list of | |
| comma-separated, quoted elements within square brackets), or a simple | |
| string of space-separated arguments. | |
| If JSON is used, you need to wrap the whole argument in quotes to | |
| prevent shell expansion and to preserve literal quotes inside the | |
| string. If a simple string is used, you need to specify the argument | |
| with `-e=`. | |
| Examples: `-e '[ "-f", "mpegts" ]'` or `-e="-f mpegts"` | |
| """ | |
| ), | |
| ) | |
| group_format.add_argument( | |
| "-e", | |
| "--extra-output-options", | |
| type=str, | |
| help=textwrap.dedent( | |
| """\ | |
| Extra output options list. | |
| A list of extra ffmpeg command line arguments. | |
| You can either use a JSON-formatted list (i.e., a list of | |
| comma-separated, quoted elements within square brackets), or a simple | |
| string of space-separated arguments. | |
| If JSON is used, you need to wrap the whole argument in quotes to | |
| prevent shell expansion and to preserve literal quotes inside the | |
| string. If a simple string is used, you need to specify the argument | |
| with `-e=`. | |
| Examples: `-e '[ "-vbr", "3" ]'` or `-e="-vbr 3"` | |
| """ | |
| ), | |
| ) | |
| group_format.add_argument( | |
| "-ofmt", | |
| "--output-format", | |
| type=str, | |
| help=textwrap.dedent( | |
| """\ | |
| Media format to use for output file(s). | |
| See 'ffmpeg -formats' for a list. | |
| If not specified, the format will be inferred by ffmpeg from the output | |
| file name. If the output file name is not explicitly specified, the | |
| extension will govern the format (see '--extension' option). | |
| """ | |
| ), | |
| ) | |
| group_format.add_argument( | |
| "-ext", | |
| "--extension", | |
| type=str, | |
| help=textwrap.dedent( | |
| """\ | |
| Output file extension to use for output files that were not explicitly | |
| specified. (Default: `mkv`) | |
| """ | |
| ), | |
| default="mkv", | |
| ) | |
| return parser | |
| def _split_options(opts): | |
| """ | |
| Parse extra options (input or output) into a list | |
| """ | |
| if not opts: | |
| return [] | |
| try: | |
| if opts.startswith("["): | |
| try: | |
| ret = [str(s) for s in json.loads(opts)] | |
| except JSONDecodeError: | |
| ret = shlex.split(opts) | |
| else: | |
| ret = shlex.split(opts) | |
| except Exception as e: | |
| raise FFmpegNormalizeError(f"Could not parse extra_options: {e}") | |
| return ret | |
| def main(): | |
| cli_args = create_parser().parse_args() | |
| if cli_args.quiet: | |
| logger.setLevel(logging.ERROR) | |
| elif cli_args.debug: | |
| logger.setLevel(logging.DEBUG) | |
| elif cli_args.verbose: | |
| logger.setLevel(logging.INFO) | |
| # parse extra options | |
| extra_input_options = _split_options(cli_args.extra_input_options) | |
| extra_output_options = _split_options(cli_args.extra_output_options) | |
| ffmpeg_normalize = FFmpegNormalize( | |
| normalization_type=cli_args.normalization_type, | |
| target_level=cli_args.target_level, | |
| print_stats=cli_args.print_stats, | |
| loudness_range_target=cli_args.loudness_range_target, | |
| # threshold=cli_args.threshold, | |
| true_peak=cli_args.true_peak, | |
| offset=cli_args.offset, | |
| dual_mono=cli_args.dual_mono, | |
| audio_codec=cli_args.audio_codec, | |
| audio_bitrate=cli_args.audio_bitrate, | |
| sample_rate=cli_args.sample_rate, | |
| keep_original_audio=cli_args.keep_original_audio, | |
| pre_filter=cli_args.pre_filter, | |
| post_filter=cli_args.post_filter, | |
| video_codec=cli_args.video_codec, | |
| video_disable=cli_args.video_disable, | |
| subtitle_disable=cli_args.subtitle_disable, | |
| metadata_disable=cli_args.metadata_disable, | |
| chapters_disable=cli_args.chapters_disable, | |
| extra_input_options=extra_input_options, | |
| extra_output_options=extra_output_options, | |
| output_format=cli_args.output_format, | |
| dry_run=cli_args.dry_run, | |
| progress=cli_args.progress, | |
| ) | |
| if ( | |
| cli_args.output is not None | |
| and len(cli_args.output) > 0 | |
| and len(cli_args.input) > len(cli_args.output) | |
| ): | |
| logger.warning( | |
| "There are more input files than output file names given. " | |
| "Please specify one output file name per input file using -o <output1> <output2> ... " | |
| "Will apply default file naming for the remaining ones." | |
| ) | |
| for index, input_file in enumerate(cli_args.input): | |
| if cli_args.output is not None and index < len(cli_args.output): | |
| if cli_args.output_folder and cli_args.output_folder != "normalized": | |
| logger.warning( | |
| "Output folder {} is ignored for input file {}".format( | |
| cli_args.output_folder, input_file | |
| ) | |
| ) | |
| output_file = cli_args.output[index] | |
| output_dir = os.path.dirname(output_file) | |
| if output_dir != "" and not os.path.isdir(output_dir): | |
| raise FFmpegNormalizeError( | |
| f"Output file path {output_dir} does not exist" | |
| ) | |
| else: | |
| output_file = os.path.join( | |
| cli_args.output_folder, | |
| os.path.splitext(os.path.basename(input_file))[0] | |
| + "." | |
| + cli_args.extension, | |
| ) | |
| if not os.path.isdir(cli_args.output_folder) and not cli_args.dry_run: | |
| logger.warning( | |
| "Output directory '{}' does not exist, will create".format( | |
| cli_args.output_folder | |
| ) | |
| ) | |
| os.makedirs(cli_args.output_folder) | |
| if os.path.exists(output_file) and not cli_args.force: | |
| logger.error( | |
| "Output file {} already exists, skipping. Use -f to force overwriting.".format( | |
| output_file | |
| ) | |
| ) | |
| else: | |
| ffmpeg_normalize.add_media_file(input_file, output_file) | |
| ffmpeg_normalize.run_normalization() | |
| if __name__ == "__main__": | |
| main() | |