Complete High-quality profile and add menus to split discs
High profile is now fully automatic instead of relying on the manual "adjust disc usage" button: - compute_high_profile_bitrate() (pure, unit-tested) picks the fewest discs that hold the content down to a quality floor, then fills each disc; a 2h movie lands on one filled disc (~3750k), a 4h movie on two, a 30min clip on one near the 9000k cap. - apply_high_profile_bitrate() applies that rate to every title and forces 2-pass; it runs before split planning so the existing splitter reuses it. Split discs now get a per-disc menu (listing that disc's titles) when the project has a menu enabled, honoring the startup/play-all settings; they still auto-play when menus are off. Fix: _adjust_audio_bitrate capped its table at 384 (an MP2 limit) and silently clamped the High profile's 448k AC3 down to 384; extend the table to AC3 rates (448/512/640) in both the ffmpeg and avconv backends. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
5fe1013be8
commit
3d1ce504b9
3 changed files with 107 additions and 9 deletions
|
|
@ -538,11 +538,14 @@ class avconv(devedeng.avbase.avbase):
|
||||||
self.command_var.append(output_file)
|
self.command_var.append(output_file)
|
||||||
|
|
||||||
def _adjust_audio_bitrate(self, bitrate):
|
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:
|
for a in br:
|
||||||
if a >= bitrate:
|
if a >= bitrate:
|
||||||
return a
|
return a
|
||||||
return 384
|
return br[-1]
|
||||||
|
|
||||||
def _avconv_text_subtitle_filter(self, file_project):
|
def _avconv_text_subtitle_filter(self, file_project):
|
||||||
""" Build a `subtitles=` filter to burn a text-based embedded subtitle
|
""" Build a `subtitles=` filter to burn a text-based embedded subtitle
|
||||||
|
|
|
||||||
|
|
@ -596,11 +596,14 @@ class ffmpeg(devedeng.executor.executor):
|
||||||
print(self.command_var)
|
print(self.command_var)
|
||||||
|
|
||||||
def _adjust_audio_bitrate(self, bitrate):
|
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:
|
for a in br:
|
||||||
if a >= bitrate:
|
if a >= bitrate:
|
||||||
return a
|
return a
|
||||||
return 384
|
return br[-1]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _escape_subtitle_path(path):
|
def _escape_subtitle_path(path):
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,39 @@ import devedeng.help
|
||||||
import devedeng.separator
|
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):
|
def _movie_chapter_seconds(movie):
|
||||||
""" Returns a sorted list of candidate chapter-cut times (seconds) for a
|
""" Returns a sorted list of candidate chapter-cut times (seconds) for a
|
||||||
movie: the automatic chapters (every chapter_size minutes) merged with
|
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)
|
plan = plan_disc_split(movies, disc_capacity, first_disc_reserved)
|
||||||
return plan
|
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):
|
def _build_profile_selector(self):
|
||||||
""" Build the DVD encode-profile selector (Compatibility / High quality
|
""" Build the DVD encode-profile selector (Compatibility / High quality
|
||||||
/ Custom) and pack it next to the disc-size combo. When "Custom" is
|
/ 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):
|
def _build_split_dvd(self, run_window, data, split_plan):
|
||||||
""" Author each disc of a split DVD project into its own subfolder and
|
""" 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
|
emit one ISO per disc (name-disc1.iso, name-disc2.iso, ...).
|
||||||
discs are authored without a menu: each plays its segment(s). """
|
|
||||||
|
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()
|
cv = devedeng.converter.converter.get_converter()
|
||||||
title = self.menu.title_text if self.menu.title_text else ""
|
title = self.menu.title_text if self.menu.title_text else ""
|
||||||
title = title.strip()
|
title = title.strip()
|
||||||
menuWide = (self.menu.menu_aspect_ratio == "menu_aspect_16_9")
|
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
|
last_iso = None
|
||||||
for disc_index, segments in enumerate(split_plan):
|
for disc_index, segments in enumerate(split_plan):
|
||||||
|
|
@ -767,12 +847,21 @@ class devede_project:
|
||||||
else:
|
else:
|
||||||
segment_lengths.append(None) # whole, unsplit movie
|
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()
|
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,
|
dvdauthor.create_dvd_project(disc_path,
|
||||||
"{:s}-disc{:d}".format(data.name, disc_no),
|
"{:s}-disc{:d}".format(data.name, disc_no),
|
||||||
seg_movies, None, False,
|
seg_movies, menu_entries,
|
||||||
False, menuWide, segment_lengths)
|
start_with_menu, play_all, menuWide,
|
||||||
|
segment_lengths)
|
||||||
for d in deps:
|
for d in deps:
|
||||||
dvdauthor.add_dependency(d)
|
dvdauthor.add_dependency(d)
|
||||||
run_window.add_process(dvdauthor)
|
run_window.add_process(dvdauthor)
|
||||||
|
|
@ -839,6 +928,9 @@ class devede_project:
|
||||||
# other formats keep their original single-image behaviour.
|
# other formats keep their original single-image behaviour.
|
||||||
split_plan = None
|
split_plan = None
|
||||||
if self.disc_type == "dvd":
|
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()
|
plan = self.plan_disc_split()
|
||||||
if len(plan) > 1:
|
if len(plan) > 1:
|
||||||
choice = self._ask_split_choice(len(plan))
|
choice = self._ask_split_choice(len(plan))
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue