Skip to content
File

Blob: firmware/crates/radio-core/tests/spectrum.rs

rust120 lines
1use radio_core::{
2 playback::{Due, Playback},
3 protocol::Action,
4 spectrum::{Continuity, FFT_SIZE, HISTORY_FLOATS, Tag, Window},
5};
6use std::num::NonZeroU32;
7 
8#[test]
9fn window_keeps_the_latest_pcm_and_applies_periodic_hann() {
10 let mut storage = vec![0.0; HISTORY_FLOATS];
11 let mut window = Window::new(&mut storage).unwrap();
12 let mut fft = vec![9.0; FFT_SIZE * 2];
13 assert!(!window.write_complex(&mut fft).unwrap());
14 let pcm: Vec<i16> = (0..FFT_SIZE * 2)
15 .flat_map(|i| [i as i16, i as i16])
16 .collect();
17 for chunk in pcm.chunks(1920) {
18 window.push_stereo(chunk).unwrap();
19 }
20 assert!(window.write_complex(&mut fft).unwrap());
21 assert_eq!(fft[0], 0.0);
22 assert!((fft[FFT_SIZE] - 3072.0 / 32768.0).abs() < 1e-6);
23 assert!((fft[FFT_SIZE / 2] - 2560.0 / 32768.0 * 0.5).abs() < 1e-6);
24 assert!(fft.chunks_exact(2).all(|pair| pair[1] == 0.0));
25 window.clear();
26 assert!(!window.write_complex(&mut fft).unwrap());
27}
28 
29#[test]
30fn band_mapping_has_known_silence_and_amplitude_calibration() {
31 let mut storage = vec![0.0; HISTORY_FLOATS];
32 let window = Window::new(&mut storage).unwrap();
33 let mut fft = vec![0.0; FFT_SIZE * 2];
34 assert_eq!(window.bands(&fft).unwrap(), [0; 32]);
35 // A periodic-Hann-windowed 1.5 kHz tone of amplitude 1/16 has a central
36 // bin magnitude N/64 = 32. Its -24.08 dBFS maps to 185/255 in band 19.
37 fft[64 * 2] = 32.0;
38 let levels = window.bands(&fft).unwrap();
39 assert_eq!(levels[19], 185);
40 assert_eq!(levels.iter().filter(|&&value| value != 0).count(), 1);
41}
42 
43#[test]
44fn invalid_buffers_are_rejected_before_mutating_history() {
45 assert!(Window::new(&mut [0.0; 3]).is_err());
46 let mut storage = vec![0.0; HISTORY_FLOATS];
47 let mut window = Window::new(&mut storage).unwrap();
48 assert!(window.push_stereo(&[1, 2, 3]).is_err());
49 assert!(window.write_complex(&mut [0.0; 8]).is_err());
50 assert!(window.bands(&[0.0; 8]).is_err());
51}
52 
53fn due(epoch: u32, pts_ms: u32) -> Due {
54 Due {
55 epoch,
56 track_index: 0,
57 revision: 0,
58 pts_ms,
59 index: 0,
60 paused: false,
61 spectrum: true,
62 }
63}
64 
65#[test]
66fn missing_packets_and_epoch_changes_reset_decoder_continuity() {
67 let mut cursor = Continuity::default();
68 assert!(cursor.begin(due(0, 0)));
69 assert!(!cursor.begin(due(0, 20)));
70 assert!(cursor.begin(due(0, 60)));
71 assert!(cursor.begin(due(1, 80)));
72 assert!(!cursor.begin(due(1, 100)));
73 cursor.clear();
74 assert!(cursor.begin(due(1, 120)));
75 assert!(cursor.begin(due(1, u32::MAX - 19)));
76 assert!(!cursor.begin(due(1, 0)));
77}
78 
79#[test]
80fn old_analysis_cannot_cross_pause_restart_or_track_loop() {
81 let mut player = Playback::new(NonZeroU32::new(4).unwrap(), 0);
82 let first = player.due(0).unwrap();
83 let tag = Tag {
84 due: first,
85 queued_us: 0,
86 };
87 assert!(tag.is_fresh(player.epoch(), 5000));
88 player.apply(Action::Pause);
89 assert!(!tag.is_fresh(player.epoch(), 5000));
90 let paused_epoch = player.epoch();
91 player.apply(Action::Pause);
92 assert_eq!(paused_epoch, player.epoch());
93 player.apply(Action::Play);
94 let playing_epoch = player.epoch();
95 assert_ne!(playing_epoch, paused_epoch);
96 player.apply(Action::Restart);
97 assert_ne!(playing_epoch, player.epoch());
98 let epoch = player.epoch();
99 for i in 1..=4 {
100 player.due(i * 20000);
101 }
102 assert_eq!(player.epoch(), epoch);
103 player.due(100_000);
104 assert_ne!(player.epoch(), epoch);
105 assert!(
106 !Tag {
107 due: due(epoch, 80),
108 queued_us: 0
109 }
110 .is_fresh(epoch, 120_001)
111 );
112 assert!(
113 !Tag {
114 due: due(epoch, 80),
115 queued_us: 100
116 }
117 .is_fresh(epoch, 99)
118 );
119}