Skip to content

Commit 66dd792

Browse files
hclivessclaude
andcommitted
3.16.1: the quality number is named for the encoder, and carried across
The slider said CRF whatever was selected. On VVenC the number is a QP, on NVENC a CQ, and it kept its value through a change of encoder, which is the wrong value: x265's CRF 23 is not VVenC's QP 23 (nearer 28) nor SVT-AV1's CRF 23 (nearer 30). The label now follows the encoder on the Video tab, the Quality tab, in the search window, the log, the queue and the output filename (_cq30, _qp32, _crf23). Switching encoders converts the number to its rough equivalent on the new scale, from one table anchored on x265 CRF 18/28 with the pairings FFmpeg's guides and the encoders' defaults suggest: x264 23 = x265 28 = SVT-AV1/libaom 35 = VP9 34 = VVenC 32, NVENC CQ tracking the software encoder of the same codec. The value is remembered against the encoder it was set for, so a detour through ProRes or stream copy converts from the last encoder that had a scale. Presets and saved settings apply the codec before the number, so theirs is never converted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QVrEsRs2c81KKkDJGftmQq
1 parent 46cd235 commit 66dd792

6 files changed

Lines changed: 148 additions & 60 deletions

File tree

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,19 @@ and a grainy film print never wanted the same one.
1515

1616
![videer processing a queue](thumb.png)
1717

18+
## Changes in 3.16.1
19+
20+
- **The quality number is named for the encoder, and carried across when the encoder changes.** The slider
21+
said *CRF* whatever was selected — on VVenC the number is a QP, on NVENC a CQ — and it kept its value
22+
through a change of encoder, which is the wrong value: x265's CRF 23 is not VVenC's QP 23 (nearer 28), nor
23+
SVT-AV1's CRF 23 (nearer 30). The label now follows the encoder — *CRF*, *CQ* or *QP* — on the Video tab,
24+
the Quality tab, in the search window, the log and the queue, and switching encoders converts the number to
25+
its rough equivalent on the new scale, using the pairings FFmpeg's own guides and the encoders' defaults
26+
suggest: x264 23 ≈ x265 28 ≈ SVT-AV1 / libaom 35 ≈ VP9 34 ≈ VVenC 32, with NVENC's CQ tracking the software
27+
encoder of the same codec. Rough is the word — it keeps the *meaning* of the number through a change of
28+
encoder, and *Match Source Quality* is still how to get the right one. Output filenames name the knob too:
29+
`_cq30` for NVENC and `_qp32` for VVC, `_crf23` as before for the rest.
30+
1831
## Changes in 3.16
1932

2033
**Two more generations of codec**

config.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
# Application info
1111
APP_NAME = "videer"
12-
APP_VERSION = "3.16"
12+
APP_VERSION = "3.16.1"
1313
WINDOW_MIN_WIDTH = 1200
1414
WINDOW_MIN_HEIGHT = 900
1515

@@ -150,6 +150,49 @@
150150
}
151151
DEFAULT_CRF_SCALE_MAX = 51
152152

153+
# What the quality number is called on each encoder. One slider drives them all, but "CRF 32" on VVenC is a
154+
# claim about a control it does not have: x264, x265, the AV1 encoders and VP9 have a CRF, NVENC has a CQ,
155+
# VVenC has a QP.
156+
QUALITY_KNOB = {
157+
"h264_nvenc": "CQ",
158+
"hevc_nvenc": "CQ",
159+
"av1_nvenc": "CQ",
160+
"libvvenc": "QP",
161+
}
162+
163+
164+
def quality_knob(video_codec) -> str:
165+
return QUALITY_KNOB.get(video_codec, "CRF")
166+
167+
168+
# Rough equivalence between the encoders' quality scales: for each, the two values that correspond to x265
169+
# CRF 18 and CRF 28 (about "visually lossless" and "streaming quality"), linear between and beyond. These are
170+
# the pairings FFmpeg's own guides and the encoders' defaults suggest — the H.265 guide's "x265 CRF 28 looks
171+
# like x264 CRF 23", SVT-AV1's default 35 and VVenC's default 32 sitting where x265's 28 does — and NVENC's
172+
# CQ tracks the software encoder of the same codec. Good enough to keep the *meaning* of the number when the
173+
# encoder changes, which is all it is used for; the right number is still the one Match Source Quality measures.
174+
QUALITY_SCALE = {
175+
"libx264": (15, 23),
176+
"libx265": (18, 28),
177+
"h264_nvenc": (15, 23),
178+
"hevc_nvenc": (18, 28),
179+
"av1_nvenc": (25, 35),
180+
"libsvtav1": (25, 35),
181+
"libaom-av1": (25, 35),
182+
"libvpx-vp9": (24, 34),
183+
"libvvenc": (24, 32),
184+
}
185+
186+
187+
def convert_quality(value: int, from_codec, to_codec) -> int:
188+
"""The value on to_codec's scale that means about what `value` meant on from_codec's; clamped to its range"""
189+
top = CRF_SCALE_MAX.get(to_codec, DEFAULT_CRF_SCALE_MAX)
190+
src, dst = QUALITY_SCALE.get(from_codec), QUALITY_SCALE.get(to_codec)
191+
if src and dst and from_codec != to_codec:
192+
position = (value - src[0]) / (src[1] - src[0])
193+
value = round(dst[0] + position * (dst[1] - dst[0]))
194+
return max(0, min(top, int(value)))
195+
153196
# Deinterlacers: display name -> key
154197
DEINTERLACERS = [
155198
("QTGMC (AviSynth+, best quality)", "qtgmc"),

models/file_models.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import logging.handlers
99
from collections import deque
1010
from typing import Optional, List, Dict, Any
11+
from config import quality_knob
1112

1213
# A damaged source encoded with -err_detect can make FFmpeg emit an error line per packet. Keeping all of them
1314
# costs gigabytes of RAM per hour and drives the machine into swap, so keep a head (what went wrong first) and
@@ -150,7 +151,8 @@ def set_output_name(self, settings: Dict[str, Any]):
150151
codec_suffix = f"_{video_codec}_{audio_codec}"
151152
quality_suffix = ""
152153
if video_codec in CRF_CODECS:
153-
quality_suffix += f"_crf{crf}"
154+
# ...and named for what it is: a CQ on NVENC, a QP on VVenC, a CRF elsewhere
155+
quality_suffix += f"_{quality_knob(video_codec).lower()}{crf}"
154156
if audio_codec not in BITRATELESS_AUDIO_CODECS:
155157
quality_suffix += f"_abr{abr}"
156158

modules/process_manager.py

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
from utils.file_utils import FileOperations
2525
from utils import childproc
2626
from config import (CONTAINER_VIDEO_CODECS, CONTAINER_AUDIO_CODECS, VIDEO_CODEC_CONTAINERS,
27-
DEFAULT_QUALITY_METRIC)
27+
DEFAULT_QUALITY_METRIC, quality_knob)
2828

2929

3030
# FFmpeg run with -progress pipe:1 reports continuously while it is working. Total silence for this long means
@@ -623,7 +623,8 @@ def _match_quality(self, file: VideoFile, index: int) -> Optional[int]:
623623
# top would be a cycle.
624624
from modules.quality_analyzer import QualitySearch, format_score
625625

626-
file.log_info("Quality match: searching for this file's own CRF")
626+
knob = quality_knob(self.settings.get('video_codec'))
627+
file.log_info(f"Quality match: searching for this file's own {knob}")
627628
self.info_signal.emit(f"Matching quality for {file.filename}…")
628629
self._phase_start = time.time()
629630

@@ -656,9 +657,9 @@ def on_step(done: int, expected: int):
656657
if not self.should_stop:
657658
reason = search.error or "no result"
658659
file.log_info(f"Quality match did not finish ({reason}); "
659-
f"using the queue's CRF {self.settings.get('crf')}")
660+
f"using the queue's {knob} {self.settings.get('crf')}")
660661
self.info_signal.emit(f"{file.filename}: quality match failed ({reason}) — "
661-
f"encoding at CRF {self.settings.get('crf')}")
662+
f"encoding at {knob} {self.settings.get('crf')}")
662663
return None
663664

664665
recommended = result.get('recommended')
@@ -668,20 +669,20 @@ def on_step(done: int, expected: int):
668669
# at it — for a target none of them met — is the opposite of what the search is for.
669670
probes = result.get('probes') or []
670671
best = max(probes, key=lambda p: p['score']) if probes else None
671-
detail = (f"the best in CRF {search.crf_low}{search.crf_high} was "
672-
f"{format_score(result['metric'], best['score'])} at CRF {best['crf']}"
672+
detail = (f"the best in {knob} {search.crf_low}{search.crf_high} was "
673+
f"{format_score(result['metric'], best['score'])} at {knob} {best['crf']}"
673674
if best else "nothing measurable")
674-
file.log_info(f"Quality match: no CRF reached {result['target']:g} ({detail}); "
675-
f"using the queue's CRF {self.settings.get('crf')}")
676-
self.info_signal.emit(f"{file.filename}: no CRF reached the target — {detail}; "
677-
f"encoding at the queue's CRF {self.settings.get('crf')}")
675+
file.log_info(f"Quality match: no {knob} reached {result['target']:g} ({detail}); "
676+
f"using the queue's {knob} {self.settings.get('crf')}")
677+
self.info_signal.emit(f"{file.filename}: no {knob} reached the target — {detail}; "
678+
f"encoding at the queue's {knob} {self.settings.get('crf')}")
678679
for note in result.get('notes', []):
679680
file.log_info(f"[quality] {note}")
680681
return None
681682

682683
crf = int(recommended['crf'])
683684
file.matched_crf = crf
684-
summary = (f"Quality match: CRF {crf} at "
685+
summary = (f"Quality match: {knob} {crf} at "
685686
f"{format_score(result['metric'], recommended['score'])} "
686687
f"(target {result['target']:g}, pooled by {result['pool']})")
687688
if result.get('estimated_size'):
@@ -1105,7 +1106,7 @@ def validate_settings(settings: Dict[str, Any]) -> List[str]:
11051106
"the frame count and will make the comparison fail.")
11061107
if settings.get('auto_match_quality'):
11071108
if video_codec not in CRF_ENCODERS:
1108-
issues.append(f"Automatic CRF matching needs a CRF-based encoder; '{video_codec}' has no CRF, "
1109+
issues.append(f"Automatic quality matching needs an encoder with a CRF, CQ or QP; '{video_codec}' has none, "
11091110
f"so every file will use the queue's setting instead.")
11101111
elif settings.get('use_avisynth'):
11111112
issues.append("Automatic CRF matching samples the source directly, without the AviSynth+ "

0 commit comments

Comments
 (0)