fn qstep(qp: f64) -> f64 {
0.625 * 2f64.powf(qp / 6.0)
}
const QCOMP: f64 = 0.6;
#[derive(Debug, Clone)]
pub struct RateControl {
target_per_frame: f64,
buffer_size: f64,
fullness: f64,
base_qp: f64,
qp_min: f64,
qp_max: f64,
k_p: f64,
k_i: f64,
avg_c_p: f64,
avg_c_i: f64,
last_qp: f64,
}
impl RateControl {
pub fn new(bitrate: u32, framerate: f32, qp: u8) -> Self {
let bitrate = bitrate as f64;
let framerate = (framerate as f64).max(1.0);
let target_per_frame = bitrate / framerate;
let buffer_size = bitrate.max(target_per_frame * 2.0); RateControl {
target_per_frame,
buffer_size,
fullness: buffer_size * 0.5,
base_qp: qp as f64,
qp_min: (qp as f64 - 18.0).max(10.0),
qp_max: (qp as f64 + 18.0).min(51.0),
k_p: 0.0,
k_i: 0.0,
avg_c_p: 0.0,
avg_c_i: 0.0,
last_qp: qp as f64,
}
}
pub fn pick_qp(&self, is_idr: bool, complexity: f64) -> u8 {
let deviation = self.fullness - self.buffer_size * 0.5;
let frames_to_correct = (self.buffer_size / self.target_per_frame).max(4.0);
let buf_target =
(self.target_per_frame - deviation / frames_to_correct).max(self.target_per_frame * 0.2);
let avg = if is_idr { self.avg_c_i } else { self.avg_c_p };
let budget = if avg > 0.0 {
buf_target * (complexity / avg).clamp(0.25, 4.0).powf(QCOMP)
} else {
buf_target
};
let k = if is_idr { self.k_i } else { self.k_p };
let qp = if k <= 0.0 {
self.base_qp + deviation / self.buffer_size * 8.0 - if is_idr { 2.0 } else { 0.0 }
} else {
4.0 + 6.0 * (k * complexity / budget).log2()
};
qp.clamp(self.last_qp - 6.0, self.last_qp + 6.0)
.clamp(self.qp_min, self.qp_max)
.round() as u8
}
pub fn update(&mut self, is_idr: bool, bits: usize, qp: u8, complexity: f64) {
let k_new = bits as f64 * qstep(qp as f64) / complexity.max(1.0);
let ema = |old: f64, new: f64| if old <= 0.0 { new } else { 0.5 * old + 0.5 * new };
if is_idr {
self.k_i = ema(self.k_i, k_new);
self.avg_c_i = ema(self.avg_c_i, complexity);
} else {
self.k_p = ema(self.k_p, k_new);
self.avg_c_p = ema(self.avg_c_p, complexity);
}
self.fullness =
(self.fullness + bits as f64 - self.target_per_frame).clamp(0.0, self.buffer_size);
self.last_qp = qp as f64;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn qstep_doubles_every_six_qp() {
assert!((qstep(28.0) / qstep(22.0) - 2.0).abs() < 1e-9);
}
#[test]
fn raises_qp_when_overshooting() {
let mut rc = RateControl::new(1_000_000, 30.0, 26);
let c = 1.0e6; let first = rc.pick_qp(true, c);
for _ in 0..30 {
let qp = rc.pick_qp(false, c);
rc.update(false, (rc.target_per_frame as usize) * 4, qp, c);
}
assert!(rc.pick_qp(false, c) > first, "QP should climb to curb overshoot");
}
#[test]
fn lowers_qp_when_undershooting() {
let mut rc = RateControl::new(1_000_000, 30.0, 40);
let c = 1.0e6;
let start = rc.pick_qp(false, c);
for _ in 0..30 {
let qp = rc.pick_qp(false, c);
rc.update(false, (rc.target_per_frame as usize) / 8, qp, c);
}
assert!(rc.pick_qp(false, c) < start, "QP should fall to use the budget");
}
#[test]
fn complex_frame_not_given_more_quality_than_simple() {
let mut rc = RateControl::new(2_000_000, 30.0, 26);
for _ in 0..20 {
let qp = rc.pick_qp(false, 1.0e6);
rc.update(false, rc.target_per_frame as usize, qp, 1.0e6);
}
let simple = rc.pick_qp(false, 0.5e6);
let complex = rc.pick_qp(false, 4.0e6);
assert!(complex >= simple);
}
}