use std::collections::VecDeque;
use std::time::{Duration, Instant};
pub const BANDS: usize = 24;
const MIDDLES: [u32; BANDS] = [
40, 52, 65, 85, 115, 145, 190, 250, 320, 420, 540, 700, 900, 1200, 1500, 2000, 2600, 3400,
4400, 5600, 7500, 9500, 12500, 16000,
];
const WIDTH: &str = "0.47";
pub const LABEL: &str = "tuitube";
pub const EVERY: Duration = Duration::from_millis(40);
const LEAD: Duration = Duration::from_millis(200);
const MAX_WAITING: usize = 16;
const FLOOR: f32 = -120.0;
const RANGE: f32 = 28.0;
const QUIETEST_TOP: f32 = -40.0;
const TOP_FALLS: f32 = 4.0;
const BOOST: [f32; BANDS] = [
14.0, 9.0, 6.0, 6.0, 4.0, 4.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 2.0, 1.0, 1.0, 4.0,
4.0, 6.0, 9.0, 14.0, 14.0,
];
const RISE: f32 = 0.75;
const FALL: f32 = 2.2;
const HUE_SPEED: f32 = 24.0;
const HUE_STEP: f32 = 12.0;
pub type Levels = [f32; BANDS];
pub fn filter() -> String {
let copies: String = (0..BANDS).map(|i| format!("[b{i}]")).collect();
let bands: Vec<String> = MIDDLES
.iter()
.enumerate()
.map(|(i, hz)| {
let pass = format!("bandpass=f={hz}:width_type=o:width={WIDTH}");
format!("[b{i}]{pass},{pass}[c{i}]")
})
.collect();
let merged: String = (0..BANDS).map(|i| format!("[c{i}]")).collect();
format!(
"@{LABEL}:lavfi=[aformat=channel_layouts=stereo,asplit[sound][copy];\
[copy]pan=mono|c0=0.5*c0+0.5*c1,asplit={BANDS}{copies};{};\
[sound]{merged}amerge=inputs={},\
astats=metadata=1:reset=2:length=0.01:measure_perchannel=RMS_level:measure_overall=none,\
pan=stereo|c0=c0|c1=c1]",
bands.join(";"),
BANDS + 1,
)
}
#[cfg_attr(not(unix), allow(dead_code))]
pub fn property() -> String {
format!("af-metadata/{LABEL}")
}
#[cfg_attr(not(unix), allow(dead_code))]
pub fn levels(data: &serde_json::Value) -> Option<Levels> {
let mut levels = [FLOOR; BANDS];
let mut found = false;
for (key, value) in data.as_object()? {
let Some(channel) = key
.strip_prefix("lavfi.astats.")
.and_then(|k| k.strip_suffix(".RMS_level"))
.and_then(|n| n.parse::<usize>().ok())
else {
continue;
};
let Some(band) = channel.checked_sub(3).filter(|&b| b < BANDS) else {
continue;
};
let db = value.as_str().and_then(|s| s.trim().parse::<f32>().ok());
levels[band] = db.filter(|db| db.is_finite()).map_or(FLOOR, |db| {
db.clamp(FLOOR, 0.0)
});
found = true;
}
found.then_some(levels)
}
#[derive(Clone, Debug)]
pub struct Spectrum {
waiting: VecDeque<(Instant, Levels)>,
heard: Option<Levels>,
heights: [f32; BANDS],
top: f32,
moved: Option<Instant>,
hue: f32,
}
impl Default for Spectrum {
fn default() -> Self {
Self {
waiting: VecDeque::with_capacity(MAX_WAITING),
heard: None,
heights: [0.0; BANDS],
top: QUIETEST_TOP,
moved: None,
hue: 0.0,
}
}
}
impl Spectrum {
pub fn push(&mut self, levels: Levels, at: Instant) {
if self.waiting.len() == MAX_WAITING {
self.waiting.pop_front();
}
self.waiting.push_back((at, levels));
}
pub fn step(&mut self, now: Instant) {
while let Some(&(at, levels)) = self.waiting.front() {
if now.saturating_duration_since(at) < LEAD {
break;
}
self.heard = Some(levels);
self.waiting.pop_front();
}
let seconds = self
.moved
.map_or(0.0, |at| now.saturating_duration_since(at).as_secs_f32())
.min(0.25);
self.moved = Some(now);
self.hue = (self.hue + HUE_SPEED * seconds) % 360.0;
let boosted = self.heard.map(|mut levels| {
for (level, boost) in levels.iter_mut().zip(BOOST) {
*level += boost;
}
levels
});
let loudest = boosted.map_or(FLOOR, |t| t.iter().copied().fold(FLOOR, f32::max));
self.top = (self.top - TOP_FALLS * seconds)
.max(loudest)
.max(QUIETEST_TOP);
let bottom = self.top - RANGE;
for (i, height) in self.heights.iter_mut().enumerate() {
let target = boosted.map_or(0.0, |t| ((t[i] - bottom) / RANGE).clamp(0.0, 1.0));
if target > *height {
*height += (target - *height) * RISE;
} else {
*height = (*height - FALL * seconds).max(target);
}
}
}
pub fn hear(&mut self, levels: Levels, now: Instant) {
self.waiting.clear();
self.heard = Some(levels);
self.step(now);
}
pub fn heights(&self) -> &[f32; BANDS] {
&self.heights
}
pub fn hue(&self) -> f32 {
(self.hue / HUE_STEP).floor() * HUE_STEP
}
}
pub fn demo_levels(seconds: f64) -> Levels {
let beat = (seconds * 2.0).fract();
let kick = (1.0 - beat * 4.0).max(0.0) as f32;
let mut levels = [FLOOR; BANDS];
for (i, level) in levels.iter_mut().enumerate() {
let x = i as f64;
let melody = (seconds * 1.3 + x * 0.35).sin() * 0.4
+ (seconds * 0.45 - x * 0.2).sin() * 0.4
+ (seconds * 3.1 + x * 2.3).sin() * 0.3;
let bass = if i < 6 {
kick * (6 - i) as f32 * 2.5
} else {
0.0
};
*level = -30.0 + melody as f32 * 7.0 + bass - BOOST[i];
}
levels
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_filter_is_one_graph_of_tuitubes_own_numbers() {
let filter = filter();
assert!(filter.starts_with("@tuitube:lavfi=["), "{filter}");
assert!(filter.ends_with(']'));
assert_eq!(filter.matches("bandpass=").count(), 2 * BANDS);
assert!(filter.contains(&format!("amerge=inputs={}", BANDS + 1)));
assert!(filter.contains("measure_perchannel=RMS_level"));
assert!(filter.ends_with("pan=stereo|c0=c0|c1=c1]"));
assert_eq!(filter.matches('[').count(), filter.matches(']').count());
assert!(!filter.contains(' ') && !filter.contains('\n'));
}
#[test]
fn levels_are_read_leniently_from_mpvs_answer() {
let answer = serde_json::json!({
"lavfi.astats.1.RMS_level": "-20.0",
"lavfi.astats.2.RMS_level": "-20.0",
"lavfi.astats.3.RMS_level": "-37.6",
"lavfi.astats.4.RMS_level": "-inf",
"lavfi.astats.5.RMS_level": "nan",
"lavfi.astats.6.RMS_level": "3.5",
"lavfi.astats.7.RMS_level": "loud",
"lavfi.astats.14.RMS_level": "-30.0",
"lavfi.astats.27.RMS_level": "-1.0",
"lavfi.astats.99999999999999999999.RMS_level": "-1.0",
"lavfi.astats.8.Peak_level": "-1.0",
"something.else": 4,
});
let read = levels(&answer).unwrap();
assert_eq!(read[0], -37.6);
assert_eq!(read[1], FLOOR, "silence");
assert_eq!(read[2], FLOOR, "not a number");
assert_eq!(read[3], 0.0, "clipping is full scale");
assert_eq!(read[4], FLOOR, "not a number");
assert_eq!(read[5], FLOOR, "only RMS levels");
assert_eq!(read[11], -30.0);
assert_eq!(levels(&serde_json::json!({})), None, "no numbers");
assert_eq!(levels(&serde_json::json!("odd")), None);
assert_eq!(levels(&serde_json::Value::Null), None);
}
#[test]
fn bars_wait_until_the_sound_is_heard_then_rise_and_fall() {
let start = Instant::now();
let mut spectrum = Spectrum::default();
let mut loud = [FLOOR; BANDS];
loud[0] = -10.0;
spectrum.push(loud, start);
spectrum.step(start + Duration::from_millis(100));
assert_eq!(spectrum.heights()[0], 0.0, "not heard yet");
spectrum.step(start + LEAD);
let risen = spectrum.heights()[0];
assert!(risen > 0.7, "a beat shows at once: {risen}");
assert_eq!(spectrum.heights()[5], 0.0, "only the bass");
spectrum.push([FLOOR; BANDS], start + LEAD);
spectrum.step(start + LEAD * 2);
let falling = spectrum.heights()[0];
assert!(falling < risen && falling > 0.0, "{falling}");
spectrum.step(start + LEAD * 2 + Duration::from_secs(2));
assert_eq!(spectrum.heights()[0], 0.0);
assert!(spectrum.heights().iter().all(|h| (0.0..=1.0).contains(h)));
}
#[test]
fn quiet_music_fills_the_bars_but_silence_doesnt() {
let start = Instant::now();
let mut quiet = Spectrum::default();
for frame in 0..400 {
let at = start + EVERY * frame;
quiet.push([-40.0; BANDS], at);
quiet.step(at);
}
let max = quiet.heights().iter().copied().fold(0.0, f32::max);
assert!(max > 0.9, "quiet music still fills them: {max}");
let mut silent = Spectrum::default();
for frame in 0..400 {
let at = start + EVERY * frame;
silent.push([-100.0; BANDS], at);
silent.step(at);
}
assert!(
silent.heights().iter().all(|h| *h == 0.0),
"{:?}",
silent.heights()
);
}
#[test]
fn answers_that_pile_up_are_dropped_oldest_first() {
let start = Instant::now();
let mut spectrum = Spectrum::default();
for i in 0..100 {
spectrum.push([FLOOR; BANDS], start + Duration::from_millis(i));
}
assert_eq!(spectrum.waiting.len(), MAX_WAITING);
}
#[test]
fn the_demo_moves() {
let a = demo_levels(1.0);
let b = demo_levels(1.3);
assert_ne!(a, b);
assert!(a.iter().all(|l| l.is_finite() && *l <= 0.0 && *l >= FLOOR));
}
}