use rlvgl_core::event::Event;
use rlvgl_core::renderer::Renderer;
use rlvgl_core::widget::{Rect, Widget};
use super::stereo::MeterWidget;
pub fn split_horizontal_n<const N: usize>(outer: Rect, gap: i32) -> [Rect; N] {
assert!(N >= 1, "split_horizontal_n requires N >= 1");
let total_gap = gap * (N as i32 - 1);
let cell_w = ((outer.width - total_gap) / N as i32).max(1);
core::array::from_fn(|i| {
let x = outer.x + i as i32 * (cell_w + gap);
let width = if i == N - 1 {
(outer.x + outer.width) - x
} else {
cell_w
};
Rect {
x,
y: outer.y,
width: width.max(1),
height: outer.height,
}
})
}
pub fn split_vertical_n<const N: usize>(outer: Rect, gap: i32) -> [Rect; N] {
assert!(N >= 1, "split_vertical_n requires N >= 1");
let total_gap = gap * (N as i32 - 1);
let cell_h = ((outer.height - total_gap) / N as i32).max(1);
core::array::from_fn(|i| {
let y = outer.y + i as i32 * (cell_h + gap);
let height = if i == N - 1 {
(outer.y + outer.height) - y
} else {
cell_h
};
Rect {
x: outer.x,
y,
width: outer.width,
height: height.max(1),
}
})
}
pub struct MultiChannel<W: MeterWidget, const N: usize> {
bounds: Rect,
gap: i32,
channels: [W; N],
}
impl<W: MeterWidget, const N: usize> MultiChannel<W, N> {
pub fn new(bounds: Rect, gap: i32, channels: [W; N]) -> Self {
Self {
bounds,
gap,
channels,
}
}
pub fn from_horizontal_factory<F>(bounds: Rect, gap: i32, mut make: F) -> Self
where
F: FnMut(usize, Rect) -> W,
{
let child_bounds = split_horizontal_n::<N>(bounds, gap);
let channels = core::array::from_fn(|i| make(i, child_bounds[i]));
Self {
bounds,
gap,
channels,
}
}
pub fn from_vertical_factory<F>(bounds: Rect, gap: i32, mut make: F) -> Self
where
F: FnMut(usize, Rect) -> W,
{
let child_bounds = split_vertical_n::<N>(bounds, gap);
let channels = core::array::from_fn(|i| make(i, child_bounds[i]));
Self {
bounds,
gap,
channels,
}
}
pub fn bounds(&self) -> Rect {
self.bounds
}
pub fn gap(&self) -> i32 {
self.gap
}
pub const fn channel_count(&self) -> usize {
N
}
pub fn channels(&self) -> &[W; N] {
&self.channels
}
pub fn channels_mut(&mut self) -> &mut [W; N] {
&mut self.channels
}
pub fn channel(&self, idx: usize) -> &W {
&self.channels[idx]
}
pub fn channel_mut(&mut self, idx: usize) -> &mut W {
&mut self.channels[idx]
}
pub fn update_n(&mut self, dbfs: &[f32; N], dt: f32) {
for (ch, &db) in self.channels.iter_mut().zip(dbfs.iter()) {
ch.update(db, dt);
}
}
pub fn update_at(&mut self, idx: usize, dbfs: f32, dt: f32) {
self.channels[idx].update(dbfs, dt);
}
pub fn reset(&mut self) {
for ch in self.channels.iter_mut() {
ch.reset();
}
}
}
impl<W: MeterWidget, const N: usize> Widget for MultiChannel<W, N> {
fn bounds(&self) -> Rect {
self.bounds
}
fn draw(&self, renderer: &mut dyn Renderer) {
for ch in &self.channels {
ch.draw(renderer);
}
}
fn handle_event(&mut self, _event: &Event) -> bool {
false
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::meters::bargraph::LedBargraph;
use crate::meters::presets::{BROADCAST_CLASSIC_BARGRAPH, DIGITAL_STUDIO_BARGRAPH};
use rlvgl_core::widget::Color;
struct OpCounter {
rects: usize,
}
impl Renderer for OpCounter {
fn fill_rect(&mut self, _r: Rect, _c: Color) {
self.rects += 1;
}
fn draw_text(&mut self, _p: (i32, i32), _t: &str, _c: Color) {}
}
#[test]
fn split_horizontal_n_partitions_exactly() {
let outer = Rect {
x: 0,
y: 0,
width: 100,
height: 50,
};
let parts: [Rect; 4] = split_horizontal_n(outer, 4);
let last = parts[3];
assert_eq!(last.x + last.width, outer.x + outer.width);
assert_eq!(parts[0].x, outer.x);
for r in &parts {
assert!(r.width > 0);
}
}
#[test]
fn split_vertical_n_partitions_exactly() {
let outer = Rect {
x: 10,
y: 20,
width: 200,
height: 600,
};
let parts: [Rect; 6] = split_vertical_n(outer, 2);
assert_eq!(parts[0].y, outer.y);
let last = parts[5];
assert_eq!(last.y + last.height, outer.y + outer.height);
for r in &parts {
assert_eq!(r.x, outer.x);
assert_eq!(r.width, outer.width);
assert!(r.height > 0);
}
}
#[test]
fn five_one_surround_six_channels() {
let outer = Rect {
x: 0,
y: 0,
width: 480,
height: 320,
};
let mut surround: MultiChannel<LedBargraph, 6> =
MultiChannel::from_horizontal_factory(outer, 4, |_idx, b| {
LedBargraph::new(b, &BROADCAST_CLASSIC_BARGRAPH)
});
assert_eq!(surround.channel_count(), 6);
let inputs = [-30.0, -28.0, -10.0, -45.0, -34.0, -34.0];
for _ in 0..120 {
surround.update_n(&inputs, 1.0 / 60.0);
}
let centre = surround.channel(2).reading_db();
let ls = surround.channel(4).reading_db();
assert!(
centre > ls + 5.0,
"centre ({}) should read meaningfully higher than Ls ({})",
centre,
ls,
);
let mut c = OpCounter { rects: 0 };
surround.draw(&mut c);
let per_child_min = 1 + BROADCAST_CLASSIC_BARGRAPH.layout.led_count as usize;
let per_child_max = per_child_min + 1; assert!(
c.rects >= 6 * per_child_min && c.rects <= 6 * per_child_max,
"expected ops in [{}, {}], got {}",
6 * per_child_min,
6 * per_child_max,
c.rects,
);
}
#[test]
fn graphic_eq_eight_bands() {
let outer = Rect {
x: 0,
y: 0,
width: 320,
height: 200,
};
let mut eq: MultiChannel<LedBargraph, 8> =
MultiChannel::from_horizontal_factory(outer, 2, |_idx, b| {
LedBargraph::new(b, &DIGITAL_STUDIO_BARGRAPH)
});
for _ in 0..120 {
for band in 0..8 {
let dbfs = if band % 2 == 0 { -10.0 } else { -40.0 };
eq.update_at(band, dbfs, 1.0 / 60.0);
}
}
for band in 0..8 {
let r = eq.channel(band).reading_db();
if band % 2 == 0 {
assert!(r > -20.0, "even band {} should be loud, got {}", band, r);
} else {
assert!(r < -20.0, "odd band {} should be quiet, got {}", band, r);
}
}
}
#[test]
fn reset_floors_all_channels() {
let outer = Rect {
x: 0,
y: 0,
width: 96,
height: 200,
};
let mut mc: MultiChannel<LedBargraph, 3> =
MultiChannel::from_horizontal_factory(outer, 2, |_, b| {
LedBargraph::new(b, &BROADCAST_CLASSIC_BARGRAPH)
});
mc.update_n(&[-5.0, -5.0, -5.0], 1.0 / 60.0);
let before = mc.channel(0).reading_db();
mc.reset();
let after = mc.channel(0).reading_db();
assert!(
after < before,
"reset should lower the reading toward floor"
);
}
#[test]
fn channel_mut_supports_per_channel_skin_swap() {
let outer = Rect {
x: 0,
y: 0,
width: 200,
height: 200,
};
let mut mc: MultiChannel<LedBargraph, 4> =
MultiChannel::from_horizontal_factory(outer, 2, |_, b| {
LedBargraph::new(b, &BROADCAST_CLASSIC_BARGRAPH)
});
mc.channel_mut(2)
.set_ballistic(rlvgl_audio_meters_core::Ballistic::DigitalPeak);
mc.update_at(2, -1.0, 1.0 / 60.0);
assert!(mc.channel(2).reading_db() > -10.0);
assert!(mc.channel(0).reading_db() < -100.0);
}
}