use alloc::format;
use rlvgl_audio_meters_core::{Ballistic, BallisticState, RelativelyGatedLufsI};
use rlvgl_core::event::Event;
use rlvgl_core::font::{FontMetrics, WidgetFont, shape_text_ltr};
use rlvgl_core::renderer::Renderer;
use rlvgl_core::widget::{Color, Rect, Widget};
use super::skin::{MeterColorId, MeterType, Skin};
const DEFAULT_TEXT: Color = Color(0xdd, 0xe2, 0xe6, 0xff);
const DEFAULT_BACKGROUND: Color = Color(0x08, 0x0a, 0x0c, 0xff);
const NOMINAL_LU_HALF_WIDTH: f32 = 0.5;
const CAUTION_LU_HALF_WIDTH: f32 = 1.5;
pub struct LufsGaugeStrict<const N: usize> {
bounds: Rect,
skin: &'static Skin,
momentary: BallisticState,
short_term: BallisticState,
integrated: RelativelyGatedLufsI<N>,
last_m: f32,
last_s: f32,
last_i: f32,
font: WidgetFont,
}
impl<const N: usize> LufsGaugeStrict<N> {
pub fn new(bounds: Rect, skin: &'static Skin) -> Self {
assert!(
matches!(skin.meter_type, MeterType::LufsGauge),
"LufsGaugeStrict requires a skin with meter_type = LufsGauge (got {:?} for skin `{}`)",
skin.meter_type,
skin.id
);
Self {
bounds,
skin,
momentary: BallisticState::new(Ballistic::LufsM),
short_term: BallisticState::new(Ballistic::LufsS),
integrated: RelativelyGatedLufsI::<N>::new(),
last_m: rlvgl_audio_meters_core::NEG_INFINITY_FLOOR_DB,
last_s: rlvgl_audio_meters_core::NEG_INFINITY_FLOOR_DB,
last_i: rlvgl_audio_meters_core::NEG_INFINITY_FLOOR_DB,
font: WidgetFont::new(),
}
}
pub fn set_font(&mut self, font: &'static dyn FontMetrics) {
self.font.set(font);
}
pub fn reset(&mut self) {
self.momentary.reset();
self.short_term.reset();
self.integrated.reset();
self.last_m = rlvgl_audio_meters_core::NEG_INFINITY_FLOOR_DB;
self.last_s = rlvgl_audio_meters_core::NEG_INFINITY_FLOOR_DB;
self.last_i = rlvgl_audio_meters_core::NEG_INFINITY_FLOOR_DB;
}
pub fn update(&mut self, dbfs: f32, dt: f32) {
self.last_m = self.momentary.update(dbfs, dt);
self.last_s = self.short_term.update(dbfs, dt);
self.last_i = self.integrated.update(dbfs, dt);
}
pub fn momentary_db(&self) -> f32 {
self.last_m
}
pub fn short_term_db(&self) -> f32 {
self.last_s
}
pub fn integrated_db(&self) -> f32 {
self.last_i
}
pub fn target_lufs(&self) -> f32 {
self.skin.scale.pivot_value
}
fn integrated_color(&self) -> Color {
let lufs = self.skin.scale.dbfs_to_scale_units(self.last_i);
let lu = lufs - self.target_lufs();
let abs = if lu < 0.0 { -lu } else { lu };
let id = if self.last_i <= self.skin.scale.range_min_db {
return self.skin.secondary.scale_text.unwrap_or(DEFAULT_TEXT);
} else if abs <= NOMINAL_LU_HALF_WIDTH {
MeterColorId::Nominal
} else if abs <= CAUTION_LU_HALF_WIDTH {
MeterColorId::Caution
} else if lu > 0.0 {
MeterColorId::Hot
} else {
MeterColorId::Safe
};
self.skin.palette.color(id)
}
}
impl<const N: usize> Widget for LufsGaugeStrict<N> {
fn bounds(&self) -> Rect {
self.bounds
}
fn widget_font_mut(&mut self) -> Option<&mut WidgetFont> {
Some(&mut self.font)
}
fn draw(&self, renderer: &mut dyn Renderer) {
let bg = self.skin.secondary.background.unwrap_or(DEFAULT_BACKGROUND);
let text_default = self.skin.secondary.scale_text.unwrap_or(DEFAULT_TEXT);
renderer.fill_rect(self.bounds, bg);
let scale = self.skin.scale;
let units = scale.label_units;
let i_lufs = scale.dbfs_to_scale_units(self.last_i);
let s_lufs = scale.dbfs_to_scale_units(self.last_s);
let m_lufs = scale.dbfs_to_scale_units(self.last_m);
let i_lu = i_lufs - self.target_lufs();
let pad = 6;
let line1_y = self.bounds.y + self.bounds.height / 3 - 4;
let line2_y = self.bounds.y + 2 * self.bounds.height / 3 - 4;
let line3_y = self.bounds.y + self.bounds.height - pad;
let x = self.bounds.x + pad;
let i_text = format!("I {i_lufs:>6.1} {units} ({i_lu:+.1} LU)");
let s_text = format!("S {s_lufs:>6.1} {units}");
let m_text = format!("M {m_lufs:>6.1} {units}");
let font = self.font.resolve();
let shaped = shape_text_ltr(font, &i_text, (x, line1_y), 0);
renderer.draw_text_shaped(&shaped, (0, 0), self.integrated_color());
let shaped = shape_text_ltr(font, &s_text, (x, line2_y), 0);
renderer.draw_text_shaped(&shaped, (0, 0), text_default);
let shaped = shape_text_ltr(font, &m_text, (x, line3_y), 0);
renderer.draw_text_shaped(&shaped, (0, 0), text_default);
}
fn handle_event(&mut self, _event: &Event) -> bool {
false
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::meters::lufs_gauge::LufsGauge;
use crate::meters::presets::LUFS_EBU_R128_GAUGE;
use rlvgl_core::font::ShapedText;
#[test]
#[should_panic(expected = "meter_type = LufsGauge")]
fn rejects_non_lufs_skin() {
let bargraph = &crate::meters::presets::BROADCAST_CLASSIC_BARGRAPH;
let _: LufsGaugeStrict<128> = LufsGaugeStrict::new(
Rect {
x: 0,
y: 0,
width: 240,
height: 120,
},
bargraph,
);
}
#[test]
fn at_target_reads_near_zero_lu() {
let mut g: LufsGaugeStrict<128> = LufsGaugeStrict::new(
Rect {
x: 0,
y: 0,
width: 240,
height: 120,
},
&LUFS_EBU_R128_GAUGE,
);
for _ in 0..1000 {
g.update(-23.0, 1.0 / 60.0);
}
let i_lu = (g.skin.scale.dbfs_to_scale_units(g.integrated_db()) - g.target_lufs()).abs();
assert!(
i_lu < 0.5,
"strict gauge at target should be within 0.5 LU, got {} LU",
i_lu,
);
}
#[test]
fn lifts_above_streaming_when_quiet_passages_present() {
let mut strict: LufsGaugeStrict<256> = LufsGaugeStrict::new(
Rect {
x: 0,
y: 0,
width: 240,
height: 120,
},
&LUFS_EBU_R128_GAUGE,
);
let mut streaming = LufsGauge::new(
Rect {
x: 0,
y: 0,
width: 240,
height: 120,
},
&LUFS_EBU_R128_GAUGE,
);
for f in 0..256 {
let dbfs = if f % 5 == 0 { -45.0 } else { -23.0 };
strict.update(dbfs, 1.0 / 60.0);
streaming.update(dbfs, 1.0 / 60.0);
}
assert!(
strict.integrated_db() > streaming.integrated_db(),
"strict ({}) should exceed streaming ({}) when quiet passages are below relative gate",
strict.integrated_db(),
streaming.integrated_db(),
);
let strict_err = (strict.integrated_db() - (-23.0)).abs();
assert!(
strict_err < 0.2,
"strict reading should track loud passage near -23, got {}",
strict.integrated_db(),
);
}
#[test]
fn draws_one_bg_plus_three_text_lines() {
let g: LufsGaugeStrict<128> = LufsGaugeStrict::new(
Rect {
x: 0,
y: 0,
width: 240,
height: 120,
},
&LUFS_EBU_R128_GAUGE,
);
struct Counter {
rects: usize,
texts: usize,
}
impl Renderer for Counter {
fn fill_rect(&mut self, _r: Rect, _c: Color) {
self.rects += 1;
}
fn draw_text_shaped(&mut self, _shaped: &ShapedText<'_>, _o: (i32, i32), _c: Color) {
self.texts += 1;
}
fn draw_text(&mut self, _p: (i32, i32), _t: &str, _c: Color) {}
}
let mut c = Counter { rects: 0, texts: 0 };
g.draw(&mut c);
assert_eq!(c.rects, 1);
assert_eq!(c.texts, 3);
}
}