From c3b98aa2b13d573c97bd35324c8de346cf5b1ed9 Mon Sep 17 00:00:00 2001 From: Werner Robitza Date: Mon, 18 Nov 2024 11:34:44 +0100 Subject: [PATCH] Bump version to 1.29.2 --- CHANGELOG.md | 7 + docs/ffmpeg_normalize.html | 1049 +++++++++++++++++----------------- ffmpeg_normalize/_version.py | 2 +- 3 files changed, 538 insertions(+), 520 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 920b125..bc4a7fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,13 @@ # Changelog +## v1.29.2 (2024-11-18) + +* Fix: show percentage with two decimal digits in progress. + +* Chore: add python 12. + + ## v1.29.1 (2024-10-22) * Fix: override argparse usage. diff --git a/docs/ffmpeg_normalize.html b/docs/ffmpeg_normalize.html index ff8b901..c973d71 100644 --- a/docs/ffmpeg_normalize.html +++ b/docs/ffmpeg_normalize.html @@ -1203,404 +1203,410 @@
Inherited Members
-
 44class MediaFile:
- 45    """
- 46    Class that holds a file, its streams and adjustments
- 47    """
- 48
- 49    def __init__(
- 50        self, ffmpeg_normalize: FFmpegNormalize, input_file: str, output_file: str
- 51    ):
- 52        """
- 53        Initialize a media file for later normalization by parsing the streams.
- 54
- 55        Args:
- 56            ffmpeg_normalize (FFmpegNormalize): reference to overall settings
- 57            input_file (str): Path to input file
- 58            output_file (str): Path to output file
- 59        """
- 60        self.ffmpeg_normalize = ffmpeg_normalize
- 61        self.skip = False
- 62        self.input_file = input_file
- 63        self.output_file = output_file
- 64        self.output_ext = os.path.splitext(output_file)[1][1:]
- 65        self.streams: StreamDict = {"audio": {}, "video": {}, "subtitle": {}}
- 66
- 67        self.parse_streams()
+            
 46class MediaFile:
+ 47    """
+ 48    Class that holds a file, its streams and adjustments
+ 49    """
+ 50
+ 51    def __init__(
+ 52        self, ffmpeg_normalize: FFmpegNormalize, input_file: str, output_file: str
+ 53    ):
+ 54        """
+ 55        Initialize a media file for later normalization by parsing the streams.
+ 56
+ 57        Args:
+ 58            ffmpeg_normalize (FFmpegNormalize): reference to overall settings
+ 59            input_file (str): Path to input file
+ 60            output_file (str): Path to output file
+ 61        """
+ 62        self.ffmpeg_normalize = ffmpeg_normalize
+ 63        self.skip = False
+ 64        self.input_file = input_file
+ 65        self.output_file = output_file
+ 66        self.output_ext = os.path.splitext(output_file)[1][1:]
+ 67        self.streams: StreamDict = {"audio": {}, "video": {}, "subtitle": {}}
  68
- 69    def _stream_ids(self) -> list[int]:
- 70        """
- 71        Get all stream IDs of this file.
- 72
- 73        Returns:
- 74            list: List of stream IDs
- 75        """
- 76        return (
- 77            list(self.streams["audio"].keys())
- 78            + list(self.streams["video"].keys())
- 79            + list(self.streams["subtitle"].keys())
- 80        )
- 81
- 82    def __repr__(self) -> str:
- 83        return os.path.basename(self.input_file)
- 84
- 85    def parse_streams(self) -> None:
- 86        """
- 87        Try to parse all input streams from file and set them in self.streams.
- 88
- 89        Raises:
- 90            FFmpegNormalizeError: If no audio streams are found
- 91        """
- 92        _logger.debug(f"Parsing streams of {self.input_file}")
- 93
- 94        cmd = [
- 95            self.ffmpeg_normalize.ffmpeg_exe,
- 96            "-i",
- 97            self.input_file,
- 98            "-c",
- 99            "copy",
-100            "-t",
-101            "0",
-102            "-map",
+ 69        self.parse_streams()
+ 70
+ 71    def _stream_ids(self) -> list[int]:
+ 72        """
+ 73        Get all stream IDs of this file.
+ 74
+ 75        Returns:
+ 76            list: List of stream IDs
+ 77        """
+ 78        return (
+ 79            list(self.streams["audio"].keys())
+ 80            + list(self.streams["video"].keys())
+ 81            + list(self.streams["subtitle"].keys())
+ 82        )
+ 83
+ 84    def __repr__(self) -> str:
+ 85        return os.path.basename(self.input_file)
+ 86
+ 87    def parse_streams(self) -> None:
+ 88        """
+ 89        Try to parse all input streams from file and set them in self.streams.
+ 90
+ 91        Raises:
+ 92            FFmpegNormalizeError: If no audio streams are found
+ 93        """
+ 94        _logger.debug(f"Parsing streams of {self.input_file}")
+ 95
+ 96        cmd = [
+ 97            self.ffmpeg_normalize.ffmpeg_exe,
+ 98            "-i",
+ 99            self.input_file,
+100            "-c",
+101            "copy",
+102            "-t",
 103            "0",
-104            "-f",
-105            "null",
-106            NUL,
-107        ]
-108
-109        output = CommandRunner().run_command(cmd).get_output()
+104            "-map",
+105            "0",
+106            "-f",
+107            "null",
+108            NUL,
+109        ]
 110
-111        _logger.debug("Stream parsing command output:")
-112        _logger.debug(output)
-113
-114        output_lines = [line.strip() for line in output.split("\n")]
+111        output = CommandRunner().run_command(cmd).get_output()
+112
+113        _logger.debug("Stream parsing command output:")
+114        _logger.debug(output)
 115
-116        duration = None
-117        for line in output_lines:
-118            if "Duration" in line:
-119                if duration_search := DUR_REGEX.search(line):
-120                    duration = _to_ms(**duration_search.groupdict()) / 1000
-121                    _logger.debug(f"Found duration: {duration} s")
-122                else:
-123                    _logger.warning("Could not extract duration from input file!")
-124
-125            if not line.startswith("Stream"):
-126                continue
-127
-128            if stream_id_match := re.search(r"#0:([\d]+)", line):
-129                stream_id = int(stream_id_match.group(1))
-130                if stream_id in self._stream_ids():
-131                    continue
-132            else:
-133                continue
-134
-135            if "Audio" in line:
-136                _logger.debug(f"Found audio stream at index {stream_id}")
-137                sample_rate_match = re.search(r"(\d+) Hz", line)
-138                sample_rate = (
-139                    int(sample_rate_match.group(1)) if sample_rate_match else None
-140                )
-141                bit_depth_match = re.search(r"[sfu](\d+)(p|le|be)?", line)
-142                bit_depth = int(bit_depth_match.group(1)) if bit_depth_match else None
-143                self.streams["audio"][stream_id] = AudioStream(
-144                    self.ffmpeg_normalize,
-145                    self,
-146                    stream_id,
-147                    sample_rate,
-148                    bit_depth,
-149                    duration,
-150                )
-151
-152            elif "Video" in line:
-153                _logger.debug(f"Found video stream at index {stream_id}")
-154                self.streams["video"][stream_id] = VideoStream(
-155                    self.ffmpeg_normalize, self, stream_id
-156                )
-157
-158            elif "Subtitle" in line:
-159                _logger.debug(f"Found subtitle stream at index {stream_id}")
-160                self.streams["subtitle"][stream_id] = SubtitleStream(
-161                    self.ffmpeg_normalize, self, stream_id
-162                )
-163
-164        if not self.streams["audio"]:
-165            raise FFmpegNormalizeError(
-166                f"Input file {self.input_file} does not contain any audio streams"
-167            )
-168
-169        if (
-170            self.output_ext.lower() in ONE_STREAM
-171            and len(self.streams["audio"].values()) > 1
-172        ):
-173            _logger.warning(
-174                "Output file only supports one stream. "
-175                "Keeping only first audio stream."
-176            )
-177            first_stream = list(self.streams["audio"].values())[0]
-178            self.streams["audio"] = {first_stream.stream_id: first_stream}
-179            self.streams["video"] = {}
-180            self.streams["subtitle"] = {}
-181
-182    def run_normalization(self) -> None:
-183        """
-184        Run the normalization process for this file.
-185        """
-186        _logger.debug(f"Running normalization for {self.input_file}")
-187
-188        # run the first pass to get loudness stats
-189        self._first_pass()
-190
-191        # run the second pass as a whole
-192        if self.ffmpeg_normalize.progress:
-193            with tqdm(total=100, position=1, desc="Second Pass") as pbar:
-194                for progress in self._second_pass():
-195                    pbar.update(progress - pbar.n)
-196        else:
-197            for _ in self._second_pass():
-198                pass
-199
-200    def _can_write_output_video(self) -> bool:
-201        """
-202        Determine whether the output file can contain video at all.
-203
-204        Returns:
-205            bool: True if the output file can contain video, False otherwise
-206        """
-207        if self.output_ext.lower() in AUDIO_ONLY_FORMATS:
-208            return False
-209
-210        return not self.ffmpeg_normalize.video_disable
-211
-212    def _first_pass(self) -> None:
-213        """
-214        Run the first pass of the normalization process.
-215        """
-216        _logger.debug(f"Parsing normalization info for {self.input_file}")
-217
-218        for index, audio_stream in enumerate(self.streams["audio"].values()):
-219            if self.ffmpeg_normalize.normalization_type == "ebu":
-220                fun = getattr(audio_stream, "parse_loudnorm_stats")
-221            else:
-222                fun = getattr(audio_stream, "parse_astats")
-223
-224            if self.ffmpeg_normalize.progress:
-225                with tqdm(
-226                    total=100,
-227                    position=1,
-228                    desc=f"Stream {index + 1}/{len(self.streams['audio'].values())}",
-229                ) as pbar:
-230                    for progress in fun():
-231                        pbar.update(progress - pbar.n)
-232            else:
-233                for _ in fun():
-234                    pass
-235
-236        # set initial stats (for dry-runs, this is the only thing we need to do)
-237        self.ffmpeg_normalize.stats = [
-238            audio_stream.get_stats() for audio_stream in self.streams["audio"].values()
-239        ]
-240
-241    def _get_audio_filter_cmd(self) -> tuple[str, list[str]]:
-242        """
-243        Return the audio filter command and output labels needed.
-244
-245        Returns:
-246            tuple[str, list[str]]: filter_complex command and the required output labels
-247        """
-248        filter_chains = []
-249        output_labels = []
-250
-251        for audio_stream in self.streams["audio"].values():
-252            if self.ffmpeg_normalize.normalization_type == "ebu":
-253                normalization_filter = audio_stream.get_second_pass_opts_ebu()
-254            else:
-255                normalization_filter = audio_stream.get_second_pass_opts_peakrms()
-256
-257            input_label = f"[0:{audio_stream.stream_id}]"
-258            output_label = f"[norm{audio_stream.stream_id}]"
-259            output_labels.append(output_label)
-260
-261            filter_chain = []
-262
-263            if self.ffmpeg_normalize.pre_filter:
-264                filter_chain.append(self.ffmpeg_normalize.pre_filter)
-265
-266            filter_chain.append(normalization_filter)
-267
-268            if self.ffmpeg_normalize.post_filter:
-269                filter_chain.append(self.ffmpeg_normalize.post_filter)
+116        output_lines = [line.strip() for line in output.split("\n")]
+117
+118        duration = None
+119        for line in output_lines:
+120            if "Duration" in line:
+121                if duration_search := DUR_REGEX.search(line):
+122                    duration = _to_ms(**duration_search.groupdict()) / 1000
+123                    _logger.debug(f"Found duration: {duration} s")
+124                else:
+125                    _logger.warning("Could not extract duration from input file!")
+126
+127            if not line.startswith("Stream"):
+128                continue
+129
+130            if stream_id_match := re.search(r"#0:([\d]+)", line):
+131                stream_id = int(stream_id_match.group(1))
+132                if stream_id in self._stream_ids():
+133                    continue
+134            else:
+135                continue
+136
+137            if "Audio" in line:
+138                _logger.debug(f"Found audio stream at index {stream_id}")
+139                sample_rate_match = re.search(r"(\d+) Hz", line)
+140                sample_rate = (
+141                    int(sample_rate_match.group(1)) if sample_rate_match else None
+142                )
+143                bit_depth_match = re.search(r"[sfu](\d+)(p|le|be)?", line)
+144                bit_depth = int(bit_depth_match.group(1)) if bit_depth_match else None
+145                self.streams["audio"][stream_id] = AudioStream(
+146                    self.ffmpeg_normalize,
+147                    self,
+148                    stream_id,
+149                    sample_rate,
+150                    bit_depth,
+151                    duration,
+152                )
+153
+154            elif "Video" in line:
+155                _logger.debug(f"Found video stream at index {stream_id}")
+156                self.streams["video"][stream_id] = VideoStream(
+157                    self.ffmpeg_normalize, self, stream_id
+158                )
+159
+160            elif "Subtitle" in line:
+161                _logger.debug(f"Found subtitle stream at index {stream_id}")
+162                self.streams["subtitle"][stream_id] = SubtitleStream(
+163                    self.ffmpeg_normalize, self, stream_id
+164                )
+165
+166        if not self.streams["audio"]:
+167            raise FFmpegNormalizeError(
+168                f"Input file {self.input_file} does not contain any audio streams"
+169            )
+170
+171        if (
+172            self.output_ext.lower() in ONE_STREAM
+173            and len(self.streams["audio"].values()) > 1
+174        ):
+175            _logger.warning(
+176                "Output file only supports one stream. "
+177                "Keeping only first audio stream."
+178            )
+179            first_stream = list(self.streams["audio"].values())[0]
+180            self.streams["audio"] = {first_stream.stream_id: first_stream}
+181            self.streams["video"] = {}
+182            self.streams["subtitle"] = {}
+183
+184    def run_normalization(self) -> None:
+185        """
+186        Run the normalization process for this file.
+187        """
+188        _logger.debug(f"Running normalization for {self.input_file}")
+189
+190        # run the first pass to get loudness stats
+191        self._first_pass()
+192
+193        # run the second pass as a whole
+194        if self.ffmpeg_normalize.progress:
+195            with tqdm(
+196                total=100,
+197                position=1,
+198                desc="Second Pass",
+199                bar_format=TQDM_BAR_FORMAT,
+200            ) as pbar:
+201                for progress in self._second_pass():
+202                    pbar.update(progress - pbar.n)
+203        else:
+204            for _ in self._second_pass():
+205                pass
+206
+207    def _can_write_output_video(self) -> bool:
+208        """
+209        Determine whether the output file can contain video at all.
+210
+211        Returns:
+212            bool: True if the output file can contain video, False otherwise
+213        """
+214        if self.output_ext.lower() in AUDIO_ONLY_FORMATS:
+215            return False
+216
+217        return not self.ffmpeg_normalize.video_disable
+218
+219    def _first_pass(self) -> None:
+220        """
+221        Run the first pass of the normalization process.
+222        """
+223        _logger.debug(f"Parsing normalization info for {self.input_file}")
+224
+225        for index, audio_stream in enumerate(self.streams["audio"].values()):
+226            if self.ffmpeg_normalize.normalization_type == "ebu":
+227                fun = getattr(audio_stream, "parse_loudnorm_stats")
+228            else:
+229                fun = getattr(audio_stream, "parse_astats")
+230
+231            if self.ffmpeg_normalize.progress:
+232                with tqdm(
+233                    total=100,
+234                    position=1,
+235                    desc=f"Stream {index + 1}/{len(self.streams['audio'].values())}",
+236                    bar_format=TQDM_BAR_FORMAT,
+237                ) as pbar:
+238                    for progress in fun():
+239                        pbar.update(progress - pbar.n)
+240            else:
+241                for _ in fun():
+242                    pass
+243
+244        # set initial stats (for dry-runs, this is the only thing we need to do)
+245        self.ffmpeg_normalize.stats = [
+246            audio_stream.get_stats() for audio_stream in self.streams["audio"].values()
+247        ]
+248
+249    def _get_audio_filter_cmd(self) -> tuple[str, list[str]]:
+250        """
+251        Return the audio filter command and output labels needed.
+252
+253        Returns:
+254            tuple[str, list[str]]: filter_complex command and the required output labels
+255        """
+256        filter_chains = []
+257        output_labels = []
+258
+259        for audio_stream in self.streams["audio"].values():
+260            if self.ffmpeg_normalize.normalization_type == "ebu":
+261                normalization_filter = audio_stream.get_second_pass_opts_ebu()
+262            else:
+263                normalization_filter = audio_stream.get_second_pass_opts_peakrms()
+264
+265            input_label = f"[0:{audio_stream.stream_id}]"
+266            output_label = f"[norm{audio_stream.stream_id}]"
+267            output_labels.append(output_label)
+268
+269            filter_chain = []
 270
-271            filter_chains.append(input_label + ",".join(filter_chain) + output_label)
-272
-273        filter_complex_cmd = ";".join(filter_chains)
-274
-275        return filter_complex_cmd, output_labels
-276
-277    def _second_pass(self) -> Iterator[float]:
-278        """
-279        Construct the second pass command and run it.
+271            if self.ffmpeg_normalize.pre_filter:
+272                filter_chain.append(self.ffmpeg_normalize.pre_filter)
+273
+274            filter_chain.append(normalization_filter)
+275
+276            if self.ffmpeg_normalize.post_filter:
+277                filter_chain.append(self.ffmpeg_normalize.post_filter)
+278
+279            filter_chains.append(input_label + ",".join(filter_chain) + output_label)
 280
-281        FIXME: make this method simpler
-282        """
-283        _logger.info(f"Running second pass for {self.input_file}")
+281        filter_complex_cmd = ";".join(filter_chains)
+282
+283        return filter_complex_cmd, output_labels
 284
-285        # get the target output stream types depending on the options
-286        output_stream_types: list[Literal["audio", "video", "subtitle"]] = ["audio"]
-287        if self._can_write_output_video():
-288            output_stream_types.append("video")
-289        if not self.ffmpeg_normalize.subtitle_disable:
-290            output_stream_types.append("subtitle")
-291
-292        # base command, here we will add all other options
-293        cmd = [self.ffmpeg_normalize.ffmpeg_exe, "-hide_banner", "-y"]
-294
-295        # extra options (if any)
-296        if self.ffmpeg_normalize.extra_input_options:
-297            cmd.extend(self.ffmpeg_normalize.extra_input_options)
-298
-299        # get complex filter command
-300        audio_filter_cmd, output_labels = self._get_audio_filter_cmd()
-301
-302        # add input file and basic filter
-303        cmd.extend(["-i", self.input_file, "-filter_complex", audio_filter_cmd])
-304
-305        # map metadata, only if needed
-306        if self.ffmpeg_normalize.metadata_disable:
-307            cmd.extend(["-map_metadata", "-1"])
-308        else:
-309            # map global metadata
-310            cmd.extend(["-map_metadata", "0"])
-311            # map per-stream metadata (e.g. language tags)
-312            for stream_type in output_stream_types:
-313                stream_key = stream_type[0]
-314                if stream_type not in self.streams:
-315                    continue
-316                for idx, _ in enumerate(self.streams[stream_type].items()):
-317                    cmd.extend(
-318                        [
-319                            f"-map_metadata:s:{stream_key}:{idx}",
-320                            f"0:s:{stream_key}:{idx}",
-321                        ]
-322                    )
-323
-324        # map chapters if needed
-325        if self.ffmpeg_normalize.chapters_disable:
-326            cmd.extend(["-map_chapters", "-1"])
-327        else:
-328            cmd.extend(["-map_chapters", "0"])
-329
-330        # collect all '-map' and codecs needed for output video based on input video
-331        if self.streams["video"]:
-332            if self._can_write_output_video():
-333                for s in self.streams["video"].keys():
-334                    cmd.extend(["-map", f"0:{s}"])
-335                # set codec (copy by default)
-336                cmd.extend(["-c:v", self.ffmpeg_normalize.video_codec])
-337            else:
-338                if not self.ffmpeg_normalize.video_disable:
-339                    _logger.warning(
-340                        f"The chosen output extension {self.output_ext} does not support video/cover art. It will be disabled."
-341                    )
-342
-343        # ... and map the output of the normalization filters
-344        for ol in output_labels:
-345            cmd.extend(["-map", ol])
-346
-347        # set audio codec (never copy)
-348        if self.ffmpeg_normalize.audio_codec:
-349            cmd.extend(["-c:a", self.ffmpeg_normalize.audio_codec])
-350        else:
-351            for index, (_, audio_stream) in enumerate(self.streams["audio"].items()):
-352                cmd.extend([f"-c:a:{index}", audio_stream.get_pcm_codec()])
-353
-354        # other audio options (if any)
-355        if self.ffmpeg_normalize.audio_bitrate:
-356            cmd.extend(["-b:a", str(self.ffmpeg_normalize.audio_bitrate)])
-357        if self.ffmpeg_normalize.sample_rate:
-358            cmd.extend(["-ar", str(self.ffmpeg_normalize.sample_rate)])
-359        if self.ffmpeg_normalize.audio_channels:
-360            cmd.extend(["-ac", str(self.ffmpeg_normalize.audio_channels)])
+285    def _second_pass(self) -> Iterator[float]:
+286        """
+287        Construct the second pass command and run it.
+288
+289        FIXME: make this method simpler
+290        """
+291        _logger.info(f"Running second pass for {self.input_file}")
+292
+293        # get the target output stream types depending on the options
+294        output_stream_types: list[Literal["audio", "video", "subtitle"]] = ["audio"]
+295        if self._can_write_output_video():
+296            output_stream_types.append("video")
+297        if not self.ffmpeg_normalize.subtitle_disable:
+298            output_stream_types.append("subtitle")
+299
+300        # base command, here we will add all other options
+301        cmd = [self.ffmpeg_normalize.ffmpeg_exe, "-hide_banner", "-y"]
+302
+303        # extra options (if any)
+304        if self.ffmpeg_normalize.extra_input_options:
+305            cmd.extend(self.ffmpeg_normalize.extra_input_options)
+306
+307        # get complex filter command
+308        audio_filter_cmd, output_labels = self._get_audio_filter_cmd()
+309
+310        # add input file and basic filter
+311        cmd.extend(["-i", self.input_file, "-filter_complex", audio_filter_cmd])
+312
+313        # map metadata, only if needed
+314        if self.ffmpeg_normalize.metadata_disable:
+315            cmd.extend(["-map_metadata", "-1"])
+316        else:
+317            # map global metadata
+318            cmd.extend(["-map_metadata", "0"])
+319            # map per-stream metadata (e.g. language tags)
+320            for stream_type in output_stream_types:
+321                stream_key = stream_type[0]
+322                if stream_type not in self.streams:
+323                    continue
+324                for idx, _ in enumerate(self.streams[stream_type].items()):
+325                    cmd.extend(
+326                        [
+327                            f"-map_metadata:s:{stream_key}:{idx}",
+328                            f"0:s:{stream_key}:{idx}",
+329                        ]
+330                    )
+331
+332        # map chapters if needed
+333        if self.ffmpeg_normalize.chapters_disable:
+334            cmd.extend(["-map_chapters", "-1"])
+335        else:
+336            cmd.extend(["-map_chapters", "0"])
+337
+338        # collect all '-map' and codecs needed for output video based on input video
+339        if self.streams["video"]:
+340            if self._can_write_output_video():
+341                for s in self.streams["video"].keys():
+342                    cmd.extend(["-map", f"0:{s}"])
+343                # set codec (copy by default)
+344                cmd.extend(["-c:v", self.ffmpeg_normalize.video_codec])
+345            else:
+346                if not self.ffmpeg_normalize.video_disable:
+347                    _logger.warning(
+348                        f"The chosen output extension {self.output_ext} does not support video/cover art. It will be disabled."
+349                    )
+350
+351        # ... and map the output of the normalization filters
+352        for ol in output_labels:
+353            cmd.extend(["-map", ol])
+354
+355        # set audio codec (never copy)
+356        if self.ffmpeg_normalize.audio_codec:
+357            cmd.extend(["-c:a", self.ffmpeg_normalize.audio_codec])
+358        else:
+359            for index, (_, audio_stream) in enumerate(self.streams["audio"].items()):
+360                cmd.extend([f"-c:a:{index}", audio_stream.get_pcm_codec()])
 361
-362        # ... and subtitles
-363        if not self.ffmpeg_normalize.subtitle_disable:
-364            for s in self.streams["subtitle"].keys():
-365                cmd.extend(["-map", f"0:{s}"])
-366            # copy subtitles
-367            cmd.extend(["-c:s", "copy"])
-368
-369        if self.ffmpeg_normalize.keep_original_audio:
-370            highest_index = len(self.streams["audio"])
-371            for index, _ in enumerate(self.streams["audio"].items()):
-372                cmd.extend(["-map", f"0:a:{index}"])
-373                cmd.extend([f"-c:a:{highest_index + index}", "copy"])
-374
-375        # extra options (if any)
-376        if self.ffmpeg_normalize.extra_output_options:
-377            cmd.extend(self.ffmpeg_normalize.extra_output_options)
-378
-379        # output format (if any)
-380        if self.ffmpeg_normalize.output_format:
-381            cmd.extend(["-f", self.ffmpeg_normalize.output_format])
+362        # other audio options (if any)
+363        if self.ffmpeg_normalize.audio_bitrate:
+364            cmd.extend(["-b:a", str(self.ffmpeg_normalize.audio_bitrate)])
+365        if self.ffmpeg_normalize.sample_rate:
+366            cmd.extend(["-ar", str(self.ffmpeg_normalize.sample_rate)])
+367        if self.ffmpeg_normalize.audio_channels:
+368            cmd.extend(["-ac", str(self.ffmpeg_normalize.audio_channels)])
+369
+370        # ... and subtitles
+371        if not self.ffmpeg_normalize.subtitle_disable:
+372            for s in self.streams["subtitle"].keys():
+373                cmd.extend(["-map", f"0:{s}"])
+374            # copy subtitles
+375            cmd.extend(["-c:s", "copy"])
+376
+377        if self.ffmpeg_normalize.keep_original_audio:
+378            highest_index = len(self.streams["audio"])
+379            for index, _ in enumerate(self.streams["audio"].items()):
+380                cmd.extend(["-map", f"0:a:{index}"])
+381                cmd.extend([f"-c:a:{highest_index + index}", "copy"])
 382
-383        # if dry run, only show sample command
-384        if self.ffmpeg_normalize.dry_run:
-385            cmd.append(self.output_file)
-386            CommandRunner(dry=True).run_command(cmd)
-387            yield 100
-388            return
-389
-390        temp_dir = mkdtemp()
-391        temp_file = os.path.join(temp_dir, f"out.{self.output_ext}")
-392        cmd.append(temp_file)
-393
-394        cmd_runner = CommandRunner()
-395        try:
-396            try:
-397                yield from cmd_runner.run_ffmpeg_command(cmd)
-398            except Exception as e:
-399                _logger.error(
-400                    f"Error while running command {shlex.join(cmd)}! Error: {e}"
-401                )
-402                raise e
-403            else:
-404                _logger.debug(
-405                    f"Moving temporary file from {temp_file} to {self.output_file}"
-406                )
-407                move(temp_file, self.output_file)
-408                rmtree(temp_dir, ignore_errors=True)
-409        except Exception as e:
-410            rmtree(temp_dir, ignore_errors=True)
-411            raise e
-412
-413        output = cmd_runner.get_output()
-414        # in the second pass, we do not normalize stream-by-stream, so we set the stats based on the
-415        # overall output (which includes multiple loudnorm stats)
-416        if self.ffmpeg_normalize.normalization_type == "ebu":
-417            all_stats = AudioStream.prune_and_parse_loudnorm_output(
-418                output, num_stats=len(self.streams["audio"])
-419            )
-420            for idx, audio_stream in enumerate(self.streams["audio"].values()):
-421                audio_stream.set_second_pass_stats(all_stats[idx])
-422
-423        # collect all stats for the final report, again (overwrite the input)
-424        self.ffmpeg_normalize.stats = [
-425            audio_stream.get_stats() for audio_stream in self.streams["audio"].values()
-426        ]
-427
-428        # warn if self.media_file.ffmpeg_normalize.dynamic == False and any of the second pass stats contain "normalization_type" == "dynamic"
-429        if self.ffmpeg_normalize.dynamic is False:
-430            for audio_stream in self.streams["audio"].values():
-431                pass2_stats = audio_stream.get_stats()["ebu_pass2"]
-432                if pass2_stats is None:
-433                    continue
-434                if pass2_stats["normalization_type"] == "dynamic":
-435                    _logger.warning(
-436                        "You specified linear normalization, but the loudnorm filter reverted to dynamic normalization. "
-437                        "This may lead to unexpected results."
-438                        "Consider your input settings, e.g. choose a lower target level or higher target loudness range."
-439                    )
-440
-441        _logger.debug("Normalization finished")
+383        # extra options (if any)
+384        if self.ffmpeg_normalize.extra_output_options:
+385            cmd.extend(self.ffmpeg_normalize.extra_output_options)
+386
+387        # output format (if any)
+388        if self.ffmpeg_normalize.output_format:
+389            cmd.extend(["-f", self.ffmpeg_normalize.output_format])
+390
+391        # if dry run, only show sample command
+392        if self.ffmpeg_normalize.dry_run:
+393            cmd.append(self.output_file)
+394            CommandRunner(dry=True).run_command(cmd)
+395            yield 100
+396            return
+397
+398        temp_dir = mkdtemp()
+399        temp_file = os.path.join(temp_dir, f"out.{self.output_ext}")
+400        cmd.append(temp_file)
+401
+402        cmd_runner = CommandRunner()
+403        try:
+404            try:
+405                yield from cmd_runner.run_ffmpeg_command(cmd)
+406            except Exception as e:
+407                _logger.error(
+408                    f"Error while running command {shlex.join(cmd)}! Error: {e}"
+409                )
+410                raise e
+411            else:
+412                _logger.debug(
+413                    f"Moving temporary file from {temp_file} to {self.output_file}"
+414                )
+415                move(temp_file, self.output_file)
+416                rmtree(temp_dir, ignore_errors=True)
+417        except Exception as e:
+418            rmtree(temp_dir, ignore_errors=True)
+419            raise e
+420
+421        output = cmd_runner.get_output()
+422        # in the second pass, we do not normalize stream-by-stream, so we set the stats based on the
+423        # overall output (which includes multiple loudnorm stats)
+424        if self.ffmpeg_normalize.normalization_type == "ebu":
+425            all_stats = AudioStream.prune_and_parse_loudnorm_output(
+426                output, num_stats=len(self.streams["audio"])
+427            )
+428            for idx, audio_stream in enumerate(self.streams["audio"].values()):
+429                audio_stream.set_second_pass_stats(all_stats[idx])
+430
+431        # collect all stats for the final report, again (overwrite the input)
+432        self.ffmpeg_normalize.stats = [
+433            audio_stream.get_stats() for audio_stream in self.streams["audio"].values()
+434        ]
+435
+436        # warn if self.media_file.ffmpeg_normalize.dynamic == False and any of the second pass stats contain "normalization_type" == "dynamic"
+437        if self.ffmpeg_normalize.dynamic is False:
+438            for audio_stream in self.streams["audio"].values():
+439                pass2_stats = audio_stream.get_stats()["ebu_pass2"]
+440                if pass2_stats is None:
+441                    continue
+442                if pass2_stats["normalization_type"] == "dynamic":
+443                    _logger.warning(
+444                        "You specified linear normalization, but the loudnorm filter reverted to dynamic normalization. "
+445                        "This may lead to unexpected results."
+446                        "Consider your input settings, e.g. choose a lower target level or higher target loudness range."
+447                    )
+448
+449        _logger.debug("Normalization finished")
 
@@ -1618,25 +1624,25 @@
Inherited Members
-
49    def __init__(
-50        self, ffmpeg_normalize: FFmpegNormalize, input_file: str, output_file: str
-51    ):
-52        """
-53        Initialize a media file for later normalization by parsing the streams.
-54
-55        Args:
-56            ffmpeg_normalize (FFmpegNormalize): reference to overall settings
-57            input_file (str): Path to input file
-58            output_file (str): Path to output file
-59        """
-60        self.ffmpeg_normalize = ffmpeg_normalize
-61        self.skip = False
-62        self.input_file = input_file
-63        self.output_file = output_file
-64        self.output_ext = os.path.splitext(output_file)[1][1:]
-65        self.streams: StreamDict = {"audio": {}, "video": {}, "subtitle": {}}
-66
-67        self.parse_streams()
+            
51    def __init__(
+52        self, ffmpeg_normalize: FFmpegNormalize, input_file: str, output_file: str
+53    ):
+54        """
+55        Initialize a media file for later normalization by parsing the streams.
+56
+57        Args:
+58            ffmpeg_normalize (FFmpegNormalize): reference to overall settings
+59            input_file (str): Path to input file
+60            output_file (str): Path to output file
+61        """
+62        self.ffmpeg_normalize = ffmpeg_normalize
+63        self.skip = False
+64        self.input_file = input_file
+65        self.output_file = output_file
+66        self.output_ext = os.path.splitext(output_file)[1][1:]
+67        self.streams: StreamDict = {"audio": {}, "video": {}, "subtitle": {}}
+68
+69        self.parse_streams()
 
@@ -1730,102 +1736,102 @@
Arguments:
-
 85    def parse_streams(self) -> None:
- 86        """
- 87        Try to parse all input streams from file and set them in self.streams.
- 88
- 89        Raises:
- 90            FFmpegNormalizeError: If no audio streams are found
- 91        """
- 92        _logger.debug(f"Parsing streams of {self.input_file}")
- 93
- 94        cmd = [
- 95            self.ffmpeg_normalize.ffmpeg_exe,
- 96            "-i",
- 97            self.input_file,
- 98            "-c",
- 99            "copy",
-100            "-t",
-101            "0",
-102            "-map",
+            
 87    def parse_streams(self) -> None:
+ 88        """
+ 89        Try to parse all input streams from file and set them in self.streams.
+ 90
+ 91        Raises:
+ 92            FFmpegNormalizeError: If no audio streams are found
+ 93        """
+ 94        _logger.debug(f"Parsing streams of {self.input_file}")
+ 95
+ 96        cmd = [
+ 97            self.ffmpeg_normalize.ffmpeg_exe,
+ 98            "-i",
+ 99            self.input_file,
+100            "-c",
+101            "copy",
+102            "-t",
 103            "0",
-104            "-f",
-105            "null",
-106            NUL,
-107        ]
-108
-109        output = CommandRunner().run_command(cmd).get_output()
+104            "-map",
+105            "0",
+106            "-f",
+107            "null",
+108            NUL,
+109        ]
 110
-111        _logger.debug("Stream parsing command output:")
-112        _logger.debug(output)
-113
-114        output_lines = [line.strip() for line in output.split("\n")]
+111        output = CommandRunner().run_command(cmd).get_output()
+112
+113        _logger.debug("Stream parsing command output:")
+114        _logger.debug(output)
 115
-116        duration = None
-117        for line in output_lines:
-118            if "Duration" in line:
-119                if duration_search := DUR_REGEX.search(line):
-120                    duration = _to_ms(**duration_search.groupdict()) / 1000
-121                    _logger.debug(f"Found duration: {duration} s")
-122                else:
-123                    _logger.warning("Could not extract duration from input file!")
-124
-125            if not line.startswith("Stream"):
-126                continue
-127
-128            if stream_id_match := re.search(r"#0:([\d]+)", line):
-129                stream_id = int(stream_id_match.group(1))
-130                if stream_id in self._stream_ids():
-131                    continue
-132            else:
-133                continue
-134
-135            if "Audio" in line:
-136                _logger.debug(f"Found audio stream at index {stream_id}")
-137                sample_rate_match = re.search(r"(\d+) Hz", line)
-138                sample_rate = (
-139                    int(sample_rate_match.group(1)) if sample_rate_match else None
-140                )
-141                bit_depth_match = re.search(r"[sfu](\d+)(p|le|be)?", line)
-142                bit_depth = int(bit_depth_match.group(1)) if bit_depth_match else None
-143                self.streams["audio"][stream_id] = AudioStream(
-144                    self.ffmpeg_normalize,
-145                    self,
-146                    stream_id,
-147                    sample_rate,
-148                    bit_depth,
-149                    duration,
-150                )
-151
-152            elif "Video" in line:
-153                _logger.debug(f"Found video stream at index {stream_id}")
-154                self.streams["video"][stream_id] = VideoStream(
-155                    self.ffmpeg_normalize, self, stream_id
-156                )
-157
-158            elif "Subtitle" in line:
-159                _logger.debug(f"Found subtitle stream at index {stream_id}")
-160                self.streams["subtitle"][stream_id] = SubtitleStream(
-161                    self.ffmpeg_normalize, self, stream_id
-162                )
-163
-164        if not self.streams["audio"]:
-165            raise FFmpegNormalizeError(
-166                f"Input file {self.input_file} does not contain any audio streams"
-167            )
-168
-169        if (
-170            self.output_ext.lower() in ONE_STREAM
-171            and len(self.streams["audio"].values()) > 1
-172        ):
-173            _logger.warning(
-174                "Output file only supports one stream. "
-175                "Keeping only first audio stream."
-176            )
-177            first_stream = list(self.streams["audio"].values())[0]
-178            self.streams["audio"] = {first_stream.stream_id: first_stream}
-179            self.streams["video"] = {}
-180            self.streams["subtitle"] = {}
+116        output_lines = [line.strip() for line in output.split("\n")]
+117
+118        duration = None
+119        for line in output_lines:
+120            if "Duration" in line:
+121                if duration_search := DUR_REGEX.search(line):
+122                    duration = _to_ms(**duration_search.groupdict()) / 1000
+123                    _logger.debug(f"Found duration: {duration} s")
+124                else:
+125                    _logger.warning("Could not extract duration from input file!")
+126
+127            if not line.startswith("Stream"):
+128                continue
+129
+130            if stream_id_match := re.search(r"#0:([\d]+)", line):
+131                stream_id = int(stream_id_match.group(1))
+132                if stream_id in self._stream_ids():
+133                    continue
+134            else:
+135                continue
+136
+137            if "Audio" in line:
+138                _logger.debug(f"Found audio stream at index {stream_id}")
+139                sample_rate_match = re.search(r"(\d+) Hz", line)
+140                sample_rate = (
+141                    int(sample_rate_match.group(1)) if sample_rate_match else None
+142                )
+143                bit_depth_match = re.search(r"[sfu](\d+)(p|le|be)?", line)
+144                bit_depth = int(bit_depth_match.group(1)) if bit_depth_match else None
+145                self.streams["audio"][stream_id] = AudioStream(
+146                    self.ffmpeg_normalize,
+147                    self,
+148                    stream_id,
+149                    sample_rate,
+150                    bit_depth,
+151                    duration,
+152                )
+153
+154            elif "Video" in line:
+155                _logger.debug(f"Found video stream at index {stream_id}")
+156                self.streams["video"][stream_id] = VideoStream(
+157                    self.ffmpeg_normalize, self, stream_id
+158                )
+159
+160            elif "Subtitle" in line:
+161                _logger.debug(f"Found subtitle stream at index {stream_id}")
+162                self.streams["subtitle"][stream_id] = SubtitleStream(
+163                    self.ffmpeg_normalize, self, stream_id
+164                )
+165
+166        if not self.streams["audio"]:
+167            raise FFmpegNormalizeError(
+168                f"Input file {self.input_file} does not contain any audio streams"
+169            )
+170
+171        if (
+172            self.output_ext.lower() in ONE_STREAM
+173            and len(self.streams["audio"].values()) > 1
+174        ):
+175            _logger.warning(
+176                "Output file only supports one stream. "
+177                "Keeping only first audio stream."
+178            )
+179            first_stream = list(self.streams["audio"].values())[0]
+180            self.streams["audio"] = {first_stream.stream_id: first_stream}
+181            self.streams["video"] = {}
+182            self.streams["subtitle"] = {}
 
@@ -1851,23 +1857,28 @@
Raises:
-
182    def run_normalization(self) -> None:
-183        """
-184        Run the normalization process for this file.
-185        """
-186        _logger.debug(f"Running normalization for {self.input_file}")
-187
-188        # run the first pass to get loudness stats
-189        self._first_pass()
-190
-191        # run the second pass as a whole
-192        if self.ffmpeg_normalize.progress:
-193            with tqdm(total=100, position=1, desc="Second Pass") as pbar:
-194                for progress in self._second_pass():
-195                    pbar.update(progress - pbar.n)
-196        else:
-197            for _ in self._second_pass():
-198                pass
+            
184    def run_normalization(self) -> None:
+185        """
+186        Run the normalization process for this file.
+187        """
+188        _logger.debug(f"Running normalization for {self.input_file}")
+189
+190        # run the first pass to get loudness stats
+191        self._first_pass()
+192
+193        # run the second pass as a whole
+194        if self.ffmpeg_normalize.progress:
+195            with tqdm(
+196                total=100,
+197                position=1,
+198                desc="Second Pass",
+199                bar_format=TQDM_BAR_FORMAT,
+200            ) as pbar:
+201                for progress in self._second_pass():
+202                    pbar.update(progress - pbar.n)
+203        else:
+204            for _ in self._second_pass():
+205                pass
 
@@ -3299,7 +3310,7 @@
Arguments:
__version__ = -'1.29.1' +'1.29.2'
diff --git a/ffmpeg_normalize/_version.py b/ffmpeg_normalize/_version.py index b6252b3..a1fbe6d 100644 --- a/ffmpeg_normalize/_version.py +++ b/ffmpeg_normalize/_version.py @@ -1 +1 @@ -__version__ = "1.29.1" +__version__ = "1.29.2"