diff --git a/src/devedeng/avconv.py b/src/devedeng/avconv.py index fb70348..af65ba8 100644 --- a/src/devedeng/avconv.py +++ b/src/devedeng/avconv.py @@ -538,11 +538,14 @@ class avconv(devedeng.avbase.avbase): self.command_var.append(output_file) def _adjust_audio_bitrate(self, bitrate): - br = [32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384] + # includes AC3 rates above the MP2 ceiling (384) so 448k AC3 isn't + # silently clamped down + br = [32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, + 448, 512, 640] for a in br: if a >= bitrate: return a - return 384 + return br[-1] def _avconv_text_subtitle_filter(self, file_project): """ Build a `subtitles=` filter to burn a text-based embedded subtitle diff --git a/src/devedeng/ffmpeg.py b/src/devedeng/ffmpeg.py index b009e65..4fb4453 100644 --- a/src/devedeng/ffmpeg.py +++ b/src/devedeng/ffmpeg.py @@ -596,11 +596,14 @@ class ffmpeg(devedeng.executor.executor): print(self.command_var) def _adjust_audio_bitrate(self, bitrate): - br = [32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384] + # includes AC3 rates above the MP2 ceiling (384) so the High profile's + # 448k AC3 isn't silently clamped down + br = [32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, + 448, 512, 640] for a in br: if a >= bitrate: return a - return 384 + return br[-1] @staticmethod def _escape_subtitle_path(path): diff --git a/src/devedeng/project.py b/src/devedeng/project.py index 53de2fa..37689ab 100644 --- a/src/devedeng/project.py +++ b/src/devedeng/project.py @@ -43,6 +43,39 @@ import devedeng.help import devedeng.separator +def compute_high_profile_bitrate(total_runtime_s, total_audio_kbits, + disc_capacity_kbytes, menu_kbytes=0.0, + cap=9000, floor=3000): + """ Pure helper for the High-quality profile (no GTK), so it can be tested. + + Strategy ("fewest discs, then fill"): use the fewest discs that can + hold the content without dropping below the quality floor (densest + packing), then raise the video bitrate to exactly fill those discs + (clamped to [floor, cap]). A short movie ends up on one disc near the + cap; a long one spreads across the minimum number of discs and fills + each, rather than spilling onto extra discs at maximum bitrate. + + Returns (n_discs, video_bitrate_kbps). + """ + if total_runtime_s <= 0: + return (1, cap) + disc_kbits = disc_capacity_kbytes * 8.0 + menu_kbits = menu_kbytes * 8.0 + audio_kbits = total_audio_kbits + # how big is the content at the LOWEST acceptable quality? that gives the + # densest packing, i.e. the fewest discs needed + content_at_floor_kbits = (floor * total_runtime_s) + audio_kbits + menu_kbits + n = max(1, int(math.ceil(content_at_floor_kbits / disc_kbits))) + # fill those n discs: spread the available video budget over the runtime + video_budget_kbits = (n * disc_kbits) - audio_kbits - menu_kbits + video_bitrate = video_budget_kbits / total_runtime_s + if video_bitrate > cap: + video_bitrate = cap + if video_bitrate < floor: + video_bitrate = floor + return (n, int(video_bitrate)) + + def _movie_chapter_seconds(movie): """ Returns a sorted list of candidate chapter-cut times (seconds) for a movie: the automatic chapters (every chapter_size minutes) merged with @@ -629,6 +662,46 @@ class devede_project: plan = plan_disc_split(movies, disc_capacity, first_disc_reserved) return plan + def apply_high_profile_bitrate(self): + """ For the High-quality profile: compute the fill-the-disc video + bitrate (fewest discs at the cap, then fill) and apply it to every + movie, and force 2-pass encoding for accurate VBR. After this the + normal split planner produces the right number of discs because the + content now fills them. No-op for non-DVD or other profiles. """ + if self.disc_type != "dvd": + return + if getattr(self.config, "encode_profile", "compatibility") != "high": + return + + movies = [m for m in self.get_all_files() + if m.element_type == "file_movie"] + if not movies: + return + + total_runtime = 0.0 + total_audio_kbits = 0.0 + for m in movies: + length = float(m.original_length) + total_runtime += length + audio_streams = max(1, getattr(m, "audio_streams", 1)) + sub_rate = 8 * len(getattr(m, "subtitles_list", [])) + # High profile uses AC3 448k audio + total_audio_kbits += (448 * audio_streams + sub_rate) * length + + disc_capacity = self.get_dvd_size()[0] * 1000.0 # kBytes + menu_kbytes = 0.0 + if self.wcreate_menu.get_active(): + menu_kbytes = float(self.menu.get_estimated_size()) + + n_discs, video_bitrate = compute_high_profile_bitrate( + total_runtime, total_audio_kbits, disc_capacity, menu_kbytes) + + for m in movies: + m.set_auto_video_audio_rate(video_bitrate, 448) + m.video_rate_automatic = True # use the computed auto rate + m.two_pass_encoding = True # VBR accuracy + self.refresh_disc_usage() + def _build_profile_selector(self): """ Build the DVD encode-profile selector (Compatibility / High quality / Custom) and pack it next to the disc-size combo. When "Custom" is @@ -730,12 +803,19 @@ class devede_project: def _build_split_dvd(self, run_window, data, split_plan): """ Author each disc of a split DVD project into its own subfolder and - emit one ISO per disc (name-disc1.iso, name-disc2.iso, ...). Split - discs are authored without a menu: each plays its segment(s). """ + emit one ISO per disc (name-disc1.iso, name-disc2.iso, ...). + + If the project has a menu enabled, each disc gets its own menu + listing that disc's titles; otherwise each disc auto-plays its + segment(s). """ cv = devedeng.converter.converter.get_converter() title = self.menu.title_text if self.menu.title_text else "" title = title.strip() menuWide = (self.menu.menu_aspect_ratio == "menu_aspect_16_9") + with_menu = (self.disc_type == "dvd") and self.wcreate_menu.get_active() + start_with_menu = with_menu and ( + self.menu.at_startup == "menu_show_at_startup") + play_all = self.menu.play_all_c last_iso = None for disc_index, segments in enumerate(split_plan): @@ -767,12 +847,21 @@ class devede_project: else: segment_lengths.append(None) # whole, unsplit movie + # build this disc's own menu from its titles, if enabled + menu_entries = None + if with_menu: + menu_procs, menu_entries = self.menu.create_dvd_menus( + seg_movies, disc_path) + for mp in menu_procs: + run_window.add_process(mp) + deps.append(mp) + dvdauthor = devedeng.dvdauthor_converter.dvdauthor_converter() - # no menu on split discs: pass menu_entries=None, start_with_menu=False dvdauthor.create_dvd_project(disc_path, "{:s}-disc{:d}".format(data.name, disc_no), - seg_movies, None, False, - False, menuWide, segment_lengths) + seg_movies, menu_entries, + start_with_menu, play_all, menuWide, + segment_lengths) for d in deps: dvdauthor.add_dependency(d) run_window.add_process(dvdauthor) @@ -839,6 +928,9 @@ class devede_project: # other formats keep their original single-image behaviour. split_plan = None if self.disc_type == "dvd": + # High profile: derive the fill-the-disc bitrate first so the split + # planner sees the real per-disc sizes (this also enables 2-pass) + self.apply_high_profile_bitrate() plan = self.plan_disc_split() if len(plan) > 1: choice = self._ask_split_choice(len(plan))