1#[derive(Debug)]
5pub struct AudioBuffers {
6 pub inputs: Vec<Vec<f32>>,
8 pub outputs: Vec<Vec<f32>>,
10 pub sample_rate: f64,
12 pub block_size: usize,
14}
15
16impl AudioBuffers {
17 pub fn new(
19 input_channels: usize,
20 output_channels: usize,
21 block_size: usize,
22 sample_rate: f64,
23 ) -> Self {
24 let inputs = vec![vec![0.0; block_size]; input_channels];
25 let outputs = vec![vec![0.0; block_size]; output_channels];
26
27 Self {
28 inputs,
29 outputs,
30 sample_rate,
31 block_size,
32 }
33 }
34
35 pub fn clear(&mut self) {
37 for buffer in &mut self.inputs {
38 buffer.fill(0.0);
39 }
40 for buffer in &mut self.outputs {
41 buffer.fill(0.0);
42 }
43 }
44
45 pub fn input_channels(&self) -> usize {
47 self.inputs.len()
48 }
49
50 pub fn output_channels(&self) -> usize {
52 self.outputs.len()
53 }
54}
55
56#[derive(Debug, Clone, Copy)]
58pub struct ChannelLevel {
59 pub peak: f32,
61 pub rms: f32,
63 pub peak_hold: f32,
65}
66
67impl Default for ChannelLevel {
68 fn default() -> Self {
69 Self {
70 peak: 0.0,
71 rms: 0.0,
72 peak_hold: 0.0,
73 }
74 }
75}
76
77impl ChannelLevel {
78 pub fn peak_db(&self) -> f32 {
80 if self.peak <= 0.0 {
81 -f32::INFINITY
82 } else {
83 20.0 * self.peak.log10()
84 }
85 }
86
87 pub fn rms_db(&self) -> f32 {
89 if self.rms <= 0.0 {
90 -f32::INFINITY
91 } else {
92 20.0 * self.rms.log10()
93 }
94 }
95
96 pub fn is_clipping(&self) -> bool {
98 self.peak > 1.0
99 }
100}
101
102#[derive(Debug, Clone)]
104pub struct AudioLevels {
105 pub channels: Vec<ChannelLevel>,
107}
108
109impl AudioLevels {
110 pub fn new(channel_count: usize) -> Self {
112 Self {
113 channels: vec![ChannelLevel::default(); channel_count],
114 }
115 }
116
117 pub fn update_from_buffers(&mut self, buffers: &[Vec<f32>]) {
119 for (i, buffer) in buffers.iter().enumerate() {
120 if i >= self.channels.len() {
121 break;
122 }
123
124 let peak = buffer.iter().map(|&x| x.abs()).fold(0.0f32, f32::max);
126
127 let sum_squares: f32 = buffer.iter().map(|&x| x * x).sum();
129 let rms = if buffer.is_empty() {
130 0.0
131 } else {
132 (sum_squares / buffer.len() as f32).sqrt()
133 };
134
135 let channel = &mut self.channels[i];
137 channel.peak = peak;
138 channel.rms = rms;
139
140 if peak > channel.peak_hold {
142 channel.peak_hold = peak;
143 }
144 }
145 }
146
147 pub fn reset_peak_hold(&mut self) {
149 for channel in &mut self.channels {
150 channel.peak_hold = channel.peak;
151 }
152 }
153
154 pub fn is_clipping(&self) -> bool {
156 self.channels.iter().any(|ch| ch.is_clipping())
157 }
158}
159
160#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
164pub struct SpeakerArrangement(pub u64);
165
166impl SpeakerArrangement {
167 pub const EMPTY: Self = Self(0);
169 pub const MONO: Self = Self(0x0008_0000);
171 pub const STEREO: Self = Self(0x3);
173 pub const STEREO_SURROUND: Self = Self(0x30);
175
176 pub fn from_raw(bits: u64) -> Self {
178 Self(bits)
179 }
180
181 pub fn raw(self) -> u64 {
183 self.0
184 }
185
186 pub fn channel_count(self) -> usize {
188 self.0.count_ones() as usize
189 }
190}
191
192#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
195pub enum MediaType {
196 Audio,
198 Event,
200}
201
202#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
205pub enum BusDirection {
206 Input,
208 Output,
210}
211
212#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
214pub struct BusArrangements {
215 pub inputs: Vec<SpeakerArrangement>,
217 pub outputs: Vec<SpeakerArrangement>,
219}
220
221#[derive(Debug, Clone)]
243pub struct PeakMeter {
244 fall_db_per_sec: f32,
245 hold: std::time::Duration,
246 level: f32,
247 peak_hold: f32,
248 peak_hold_at: Option<std::time::Instant>,
249 last: Option<std::time::Instant>,
250}
251
252impl PeakMeter {
253 const SILENCE: f32 = 1e-5;
256
257 pub fn new(fall_db_per_sec: f32, hold: std::time::Duration) -> Self {
261 Self {
262 fall_db_per_sec: fall_db_per_sec.max(0.0),
263 hold,
264 level: 0.0,
265 peak_hold: 0.0,
266 peak_hold_at: None,
267 last: None,
268 }
269 }
270
271 fn decay(&self, dt: std::time::Duration) -> f32 {
273 let db = self.fall_db_per_sec * dt.as_secs_f32();
274 10f32.powf(-db / 20.0)
275 }
276
277 pub fn push(&mut self, block_peak: f32, now: std::time::Instant) {
281 let block_peak = if block_peak.is_finite() {
284 block_peak.max(0.0)
285 } else {
286 0.0
287 };
288 let decay = match self.last {
289 Some(prev) => self.decay(now.saturating_duration_since(prev)),
290 None => 1.0,
291 };
292
293 self.level = (self.level * decay).max(block_peak);
294 if self.level < Self::SILENCE {
295 self.level = 0.0;
296 }
297
298 if block_peak >= self.peak_hold {
299 self.peak_hold = block_peak;
301 self.peak_hold_at = Some(now);
302 } else if self
303 .peak_hold_at
304 .is_some_and(|at| now.saturating_duration_since(at) > self.hold)
305 {
306 self.peak_hold = (self.peak_hold * decay).max(self.level);
308 if self.peak_hold < Self::SILENCE {
309 self.peak_hold = 0.0;
310 }
311 }
312
313 self.last = Some(now);
314 }
315
316 pub fn level(&self) -> f32 {
318 self.level
319 }
320
321 pub fn peak_hold(&self) -> f32 {
323 self.peak_hold
324 }
325
326 pub fn reset(&mut self) {
328 self.level = 0.0;
329 self.peak_hold = 0.0;
330 self.peak_hold_at = None;
331 self.last = None;
332 }
333}
334
335#[derive(Debug, Clone)]
349pub struct RmsWindow {
350 capacity: usize,
351 squares: std::collections::VecDeque<f32>,
352 sum: f64,
355}
356
357impl RmsWindow {
358 pub fn new(window_samples: usize) -> Self {
360 let capacity = window_samples.max(1);
361 Self {
362 capacity,
363 squares: std::collections::VecDeque::with_capacity(capacity),
364 sum: 0.0,
365 }
366 }
367
368 pub fn from_duration(window_secs: f32, sample_rate: f64) -> Self {
370 Self::new((window_secs.max(0.0) as f64 * sample_rate).round() as usize)
371 }
372
373 pub fn push_sample(&mut self, sample: f32) {
375 let sq = sample * sample;
376 if self.squares.len() == self.capacity {
377 if let Some(old) = self.squares.pop_front() {
378 self.sum -= old as f64;
379 }
380 }
381 self.squares.push_back(sq);
382 self.sum += sq as f64;
383 }
384
385 pub fn push_block(&mut self, block: &[f32]) {
387 for &s in block {
388 self.push_sample(s);
389 }
390 }
391
392 pub fn rms(&self) -> f32 {
394 if self.squares.is_empty() {
395 return 0.0;
396 }
397 (self.sum.max(0.0) / self.squares.len() as f64).sqrt() as f32
399 }
400
401 pub fn len(&self) -> usize {
403 self.squares.len()
404 }
405
406 pub fn is_empty(&self) -> bool {
408 self.squares.is_empty()
409 }
410
411 pub fn clear(&mut self) {
413 self.squares.clear();
414 self.sum = 0.0;
415 }
416}
417
418#[derive(Debug, Clone, Copy)]
420pub struct AudioConfig {
421 pub sample_rate: f64,
423 pub block_size: usize,
425 pub input_channels: usize,
427 pub output_channels: usize,
429 pub tempo: f64,
432 pub time_sig_numerator: i32,
434 pub time_sig_denominator: i32,
437}
438
439impl Default for AudioConfig {
440 fn default() -> Self {
441 Self {
442 sample_rate: 44100.0,
443 block_size: 512,
444 input_channels: 0,
445 output_channels: 2,
446 tempo: 120.0,
447 time_sig_numerator: 4,
448 time_sig_denominator: 4,
449 }
450 }
451}
452
453pub trait AudioStream: Send {
455 fn play(&self) -> Result<(), Box<dyn std::error::Error>>;
457
458 fn pause(&self) -> Result<(), Box<dyn std::error::Error>>;
460}
461
462#[allow(clippy::type_complexity)] pub trait AudioBackend: Send + Sync {
465 type Stream: AudioStream + Send + 'static;
467 type Device: Send + Sync;
469 type Error: std::error::Error + Send + Sync + 'static;
471
472 fn enumerate_output_devices(&self) -> Result<Vec<Self::Device>, Self::Error>;
474
475 fn enumerate_input_devices(&self) -> Result<Vec<Self::Device>, Self::Error>;
477
478 fn default_output_device(&self) -> Option<Self::Device>;
480
481 fn default_input_device(&self) -> Option<Self::Device>;
483
484 fn create_output_stream(
486 &self,
487 device: &Self::Device,
488 config: AudioConfig,
489 data_callback: Box<dyn FnMut(&mut [f32]) + Send>,
490 error_callback: Box<dyn FnMut(Self::Error) + Send>,
491 ) -> Result<Self::Stream, Self::Error>;
492
493 fn create_input_stream(
495 &self,
496 device: &Self::Device,
497 config: AudioConfig,
498 data_callback: Box<dyn FnMut(&[f32]) + Send>,
499 error_callback: Box<dyn FnMut(Self::Error) + Send>,
500 ) -> Result<Self::Stream, Self::Error>;
501}
502
503pub fn write_wav<P: AsRef<std::path::Path>>(
508 path: P,
509 channels: &[Vec<f32>],
510 sample_rate: u32,
511) -> crate::error::Result<()> {
512 use crate::error::Error;
513 use std::io::Write;
514
515 let num_channels = channels.len().max(1) as u16;
516 let frames = channels.iter().map(|c| c.len()).min().unwrap_or(0);
517 let bits_per_sample: u16 = 32;
518 let block_align = num_channels * (bits_per_sample / 8);
519 let byte_rate = sample_rate * block_align as u32;
520 let data_size = (frames * num_channels as usize * (bits_per_sample / 8) as usize) as u32;
521
522 let mut buf: Vec<u8> = Vec::with_capacity(44 + data_size as usize);
523 buf.extend_from_slice(b"RIFF");
524 buf.extend_from_slice(&(36 + data_size).to_le_bytes());
525 buf.extend_from_slice(b"WAVE");
526 buf.extend_from_slice(b"fmt ");
527 buf.extend_from_slice(&16u32.to_le_bytes());
528 buf.extend_from_slice(&3u16.to_le_bytes()); buf.extend_from_slice(&num_channels.to_le_bytes());
530 buf.extend_from_slice(&sample_rate.to_le_bytes());
531 buf.extend_from_slice(&byte_rate.to_le_bytes());
532 buf.extend_from_slice(&block_align.to_le_bytes());
533 buf.extend_from_slice(&bits_per_sample.to_le_bytes());
534 buf.extend_from_slice(b"data");
535 buf.extend_from_slice(&data_size.to_le_bytes());
536 for f in 0..frames {
538 for ch in channels {
539 buf.extend_from_slice(&ch[f].to_le_bytes());
540 }
541 }
542
543 let mut file =
544 std::fs::File::create(path).map_err(|e| Error::Other(format!("create wav: {e}")))?;
545 file.write_all(&buf)
546 .map_err(|e| Error::Other(format!("write wav: {e}")))?;
547 Ok(())
548}
549
550pub fn read_wav<P: AsRef<std::path::Path>>(path: P) -> crate::error::Result<(Vec<Vec<f32>>, u32)> {
554 use crate::error::Error;
555 let data = std::fs::read(path).map_err(|e| Error::Other(format!("read wav: {e}")))?;
556 let err = |m: &str| Error::Other(format!("invalid wav: {m}"));
557 if data.len() < 44 || &data[0..4] != b"RIFF" || &data[8..12] != b"WAVE" {
558 return Err(err("not a RIFF/WAVE file"));
559 }
560 let (mut fmt_tag, mut channels, mut sample_rate, mut bits) = (0u16, 0u16, 0u32, 0u16);
562 let mut data_range: Option<(usize, usize)> = None;
563 let mut pos = 12;
564 while pos + 8 <= data.len() {
565 let id = &data[pos..pos + 4];
566 let size = u32::from_le_bytes([data[pos + 4], data[pos + 5], data[pos + 6], data[pos + 7]])
567 as usize;
568 let body = pos + 8;
569 if id == b"fmt " && body + 16 <= data.len() {
570 fmt_tag = u16::from_le_bytes([data[body], data[body + 1]]);
571 channels = u16::from_le_bytes([data[body + 2], data[body + 3]]);
572 sample_rate = u32::from_le_bytes([
573 data[body + 4],
574 data[body + 5],
575 data[body + 6],
576 data[body + 7],
577 ]);
578 bits = u16::from_le_bytes([data[body + 14], data[body + 15]]);
579 } else if id == b"data" {
580 data_range = Some((body, (body + size).min(data.len())));
581 }
582 pos = body + size + (size & 1); }
584 let (ds, de) = data_range.ok_or_else(|| err("no data chunk"))?;
585 if channels == 0 {
586 return Err(err("zero channels"));
587 }
588 let nch = channels as usize;
589 let mut out: Vec<Vec<f32>> = vec![Vec::new(); nch];
590 let bytes = &data[ds..de];
591 match (fmt_tag, bits) {
592 (3, 32) => {
593 for (i, frame) in bytes.chunks_exact(4 * nch).enumerate() {
594 let _ = i;
595 for (ch, s) in frame.chunks_exact(4).enumerate() {
596 out[ch].push(f32::from_le_bytes([s[0], s[1], s[2], s[3]]));
597 }
598 }
599 }
600 (1, 16) => {
601 for frame in bytes.chunks_exact(2 * nch) {
602 for (ch, s) in frame.chunks_exact(2).enumerate() {
603 let v = i16::from_le_bytes([s[0], s[1]]) as f32 / 32768.0;
604 out[ch].push(v);
605 }
606 }
607 }
608 _ => return Err(err("unsupported format (need 32-bit float or 16-bit PCM)")),
609 }
610 Ok((out, sample_rate))
611}
612
613pub trait InputSource: Send {
616 fn fill(&mut self, inputs: &mut [Vec<f32>], frames: usize, sample_rate: f64);
618}
619
620#[derive(Debug, Clone)]
623pub enum SignalSource {
624 Silence,
626 Sine {
628 freq: f32,
630 amplitude: f32,
632 phase: f64,
634 },
635 WhiteNoise {
637 amplitude: f32,
639 rng: u64,
641 },
642 Wav {
644 samples: std::sync::Arc<Vec<Vec<f32>>>,
646 pos: usize,
648 looping: bool,
650 },
651}
652
653impl SignalSource {
654 pub fn sine(freq: f32, amplitude: f32) -> Self {
656 SignalSource::Sine {
657 freq,
658 amplitude,
659 phase: 0.0,
660 }
661 }
662 pub fn white_noise(amplitude: f32) -> Self {
664 SignalSource::WhiteNoise {
665 amplitude,
666 rng: 0x9E37_79B9_7F4A_7C15,
667 }
668 }
669 pub fn wav(samples: Vec<Vec<f32>>, looping: bool) -> Self {
671 SignalSource::Wav {
672 samples: std::sync::Arc::new(samples),
673 pos: 0,
674 looping,
675 }
676 }
677}
678
679impl InputSource for SignalSource {
680 fn fill(&mut self, inputs: &mut [Vec<f32>], frames: usize, sample_rate: f64) {
681 for ch in inputs.iter_mut() {
682 if ch.len() < frames {
683 ch.resize(frames, 0.0);
684 }
685 }
686 match self {
687 SignalSource::Silence => {
688 for ch in inputs.iter_mut() {
689 for s in &mut ch[..frames] {
690 *s = 0.0;
691 }
692 }
693 }
694 SignalSource::Sine {
695 freq,
696 amplitude,
697 phase,
698 } => {
699 let step = std::f64::consts::TAU * *freq as f64 / sample_rate.max(1.0);
700 for f in 0..frames {
701 let v = (phase.sin() as f32) * *amplitude;
702 for ch in inputs.iter_mut() {
703 ch[f] = v;
704 }
705 *phase = (*phase + step) % std::f64::consts::TAU;
706 }
707 }
708 SignalSource::WhiteNoise { amplitude, rng } => {
709 for f in 0..frames {
710 let mut x = *rng;
712 x ^= x << 13;
713 x ^= x >> 7;
714 x ^= x << 17;
715 *rng = x;
716 let unit = ((x >> 11) as f64 / (1u64 << 53) as f64) as f32 * 2.0 - 1.0;
718 let v = unit * *amplitude;
719 for ch in inputs.iter_mut() {
720 ch[f] = v;
721 }
722 }
723 }
724 SignalSource::Wav {
725 samples,
726 pos,
727 looping,
728 } => {
729 let total = samples.iter().map(|c| c.len()).max().unwrap_or(0);
730 for f in 0..frames {
731 let p = *pos + f;
732 let src_idx = if total == 0 {
733 None
734 } else if p < total {
735 Some(p)
736 } else if *looping {
737 Some(p % total)
738 } else {
739 None
740 };
741 for (ci, ch) in inputs.iter_mut().enumerate() {
742 ch[f] = match src_idx {
743 Some(i) => samples
744 .get(ci % samples.len().max(1))
745 .and_then(|c| c.get(i))
746 .copied()
747 .unwrap_or(0.0),
748 None => 0.0,
749 };
750 }
751 }
752 *pos += frames;
753 }
754 }
755 }
756}
757
758#[cfg(test)]
759mod wav_tests {
760 use super::*;
761
762 #[test]
763 fn write_wav_has_correct_header_and_size() {
764 let ch = vec![vec![0.0f32, 0.5, -0.5, 1.0], vec![0.1, 0.2, 0.3, 0.4]];
765 let path = std::env::temp_dir().join("vh_write_wav_test.wav");
766 write_wav(&path, &ch, 48_000).unwrap();
767 let bytes = std::fs::read(&path).unwrap();
768 let _ = std::fs::remove_file(&path);
769
770 assert_eq!(&bytes[0..4], b"RIFF");
771 assert_eq!(&bytes[8..12], b"WAVE");
772 assert_eq!(u16::from_le_bytes([bytes[20], bytes[21]]), 3); assert_eq!(u16::from_le_bytes([bytes[22], bytes[23]]), 2); assert_eq!(
775 u32::from_le_bytes([bytes[24], bytes[25], bytes[26], bytes[27]]),
776 48_000
777 );
778 assert_eq!(bytes.len(), 44 + 32);
780 }
781
782 #[test]
783 fn write_then_read_wav_round_trips() {
784 let ch = vec![vec![0.0f32, 0.5, -0.5, 1.0], vec![0.1, 0.2, 0.3, 0.4]];
785 let path = std::env::temp_dir().join(format!("vh_rw_{}.wav", std::process::id()));
786 write_wav(&path, &ch, 44_100).unwrap();
787 let (back, sr) = read_wav(&path).unwrap();
788 let _ = std::fs::remove_file(&path);
789 assert_eq!(sr, 44_100);
790 assert_eq!(back.len(), 2);
791 for (a, b) in ch.iter().zip(back.iter()) {
792 for (x, y) in a.iter().zip(b.iter()) {
793 assert!((x - y).abs() < 1e-6, "{x} vs {y}");
794 }
795 }
796 }
797}
798
799#[cfg(test)]
800mod signal_tests {
801 use super::*;
802
803 #[test]
804 fn sine_starts_at_zero_and_stays_in_amplitude() {
805 let mut src = SignalSource::sine(1000.0, 0.5);
806 let mut inputs = vec![vec![0.0f32; 256], vec![0.0f32; 256]];
807 src.fill(&mut inputs, 256, 48_000.0);
808 assert!(inputs[0][0].abs() < 1e-6, "sine should start at phase 0");
809 for ch in &inputs {
810 assert!(
811 ch.iter().all(|s| s.abs() <= 0.5 + 1e-6),
812 "exceeds amplitude"
813 );
814 }
815 assert_eq!(inputs[0], inputs[1]);
817 assert!(inputs[0].iter().any(|s| s.abs() > 0.1));
819 }
820
821 #[test]
822 fn noise_is_bounded_and_varied() {
823 let mut src = SignalSource::white_noise(0.25);
824 let mut inputs = vec![vec![0.0f32; 512]];
825 src.fill(&mut inputs, 512, 48_000.0);
826 assert!(inputs[0].iter().all(|s| s.abs() <= 0.25 + 1e-6));
827 let first = inputs[0][0];
828 assert!(inputs[0].iter().any(|&s| s != first), "noise should vary");
829 }
830
831 #[test]
832 fn wav_source_advances_and_zero_pads() {
833 let mut src = SignalSource::wav(vec![vec![1.0, 2.0, 3.0]], false);
834 let mut inputs = vec![vec![0.0f32; 5]];
835 src.fill(&mut inputs, 5, 48_000.0);
836 assert_eq!(inputs[0], vec![1.0, 2.0, 3.0, 0.0, 0.0]); }
838
839 #[test]
840 fn wav_source_loops() {
841 let mut src = SignalSource::wav(vec![vec![1.0, 2.0]], true);
842 let mut inputs = vec![vec![0.0f32; 5]];
843 src.fill(&mut inputs, 5, 48_000.0);
844 assert_eq!(inputs[0], vec![1.0, 2.0, 1.0, 2.0, 1.0]); }
846}
847
848#[cfg(test)]
849mod speaker_arrangement_tests {
850 use super::*;
851
852 #[test]
853 fn channel_counts_match_bitmask() {
854 assert_eq!(SpeakerArrangement::EMPTY.channel_count(), 0);
855 assert_eq!(SpeakerArrangement::MONO.channel_count(), 1);
856 assert_eq!(SpeakerArrangement::STEREO.channel_count(), 2);
857 assert_eq!(SpeakerArrangement::STEREO_SURROUND.channel_count(), 2);
858 }
859
860 #[test]
861 fn raw_round_trips() {
862 let bits = SpeakerArrangement::STEREO.raw();
863 assert_eq!(bits, 0x3);
864 assert_eq!(
865 SpeakerArrangement::from_raw(bits),
866 SpeakerArrangement::STEREO
867 );
868 assert_eq!(SpeakerArrangement::from_raw(0b111111).channel_count(), 6);
870 }
871
872 #[test]
873 fn media_type_and_bus_direction_serde_round_trip() {
874 for mt in [MediaType::Audio, MediaType::Event] {
875 let json = serde_json::to_string(&mt).expect("serialize MediaType");
876 let back: MediaType = serde_json::from_str(&json).expect("deserialize MediaType");
877 assert_eq!(mt, back);
878 }
879 for dir in [BusDirection::Input, BusDirection::Output] {
880 let json = serde_json::to_string(&dir).expect("serialize BusDirection");
881 let back: BusDirection = serde_json::from_str(&json).expect("deserialize BusDirection");
882 assert_eq!(dir, back);
883 }
884 }
885}
886
887#[cfg(test)]
888mod meter_tests {
889 use super::*;
890 use std::time::{Duration, Instant};
891
892 #[test]
893 fn peak_meter_rises_instantly_and_holds() {
894 let mut m = PeakMeter::new(20.0, Duration::from_secs(2));
895 let t0 = Instant::now();
896 m.push(0.7, t0);
897 assert_eq!(m.level(), 0.7);
898 assert_eq!(m.peak_hold(), 0.7);
899
900 m.push(0.9, t0 + Duration::from_millis(10));
902 assert_eq!(m.level(), 0.9);
903 assert_eq!(m.peak_hold(), 0.9);
904 }
905
906 #[test]
907 fn peak_meter_level_falls_but_hold_latches() {
908 let mut m = PeakMeter::new(20.0, Duration::from_secs(3));
909 let t0 = Instant::now();
910 m.push(1.0, t0);
911
912 m.push(0.0, t0 + Duration::from_millis(500));
914 let lvl = m.level();
915 assert!(
916 lvl < 1.0 && lvl > 0.0,
917 "level should be mid-fall, got {lvl}"
918 );
919 assert!((lvl - 0.316).abs() < 0.02, "≈-10 dB expected, got {lvl}");
920 assert_eq!(m.peak_hold(), 1.0, "hold must latch within its window");
921 }
922
923 #[test]
924 fn peak_meter_hold_falls_after_window() {
925 let mut m = PeakMeter::new(20.0, Duration::from_secs(1));
926 let t0 = Instant::now();
927 m.push(1.0, t0);
928 m.push(0.0, t0 + Duration::from_millis(1500));
930 assert!(
931 m.peak_hold() < 1.0,
932 "hold should fall after the window expired, got {}",
933 m.peak_hold()
934 );
935 }
936
937 #[test]
938 fn peak_meter_reaches_silence_floor() {
939 let mut m = PeakMeter::new(60.0, Duration::from_millis(0));
940 let t0 = Instant::now();
941 m.push(0.5, t0);
942 m.push(0.0, t0 + Duration::from_secs(10));
944 assert_eq!(m.level(), 0.0);
945 assert_eq!(m.peak_hold(), 0.0);
946 }
947
948 #[test]
949 fn rms_window_constant_signal() {
950 let mut r = RmsWindow::new(8);
951 for _ in 0..8 {
952 r.push_sample(0.5);
953 }
954 assert!((r.rms() - 0.5).abs() < 1e-6);
955 assert_eq!(r.len(), 8);
956 }
957
958 #[test]
959 fn rms_window_slides_and_evicts() {
960 let mut r = RmsWindow::new(3);
961 r.push_block(&[1.0, 1.0, 1.0]);
962 assert!((r.rms() - 1.0).abs() < 1e-6);
963 r.push_block(&[0.0, 0.0, 0.0]);
965 assert_eq!(r.len(), 3);
966 assert!(
967 r.rms() < 1e-6,
968 "window should have slid to silence, got {}",
969 r.rms()
970 );
971 }
972
973 #[test]
974 fn rms_window_empty_is_zero() {
975 let r = RmsWindow::new(16);
976 assert!(r.is_empty());
977 assert_eq!(r.rms(), 0.0);
978 }
979
980 #[test]
981 fn rms_window_from_duration_sizes_correctly() {
982 let r = RmsWindow::from_duration(0.01, 48_000.0);
984 assert_eq!(r.capacity, 480);
985 }
986}