1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
//! Signal quality metrics — SNR, dropout rate, jitter statistics.
//!
//! Attach a `SignalQuality` monitor to a stream to track real-time quality.
use std::collections::VecDeque;
/// Rolling signal-quality statistics for a stream.
#[derive(Clone, Debug)]
pub struct SignalQuality {
/// Nominal sample rate (Hz), 0 for irregular
srate: f64,
/// Window of recent inter-sample intervals
intervals: VecDeque<f64>,
/// Max window size
window: usize,
/// Last timestamp seen
last_ts: f64,
/// Total samples received
pub total_samples: u64,
/// Total dropouts detected (gap > 1.5× expected interval)
pub total_dropouts: u64,
/// Running sum/sum² for channel amplitude (for SNR)
ch_sum: Vec<f64>,
ch_sum2: Vec<f64>,
ch_count: u64,
}
/// Snapshot of quality metrics.
#[derive(Clone, Debug, Default)]
pub struct QualitySnapshot {
/// Actual effective sample rate
pub effective_srate: f64,
/// Mean jitter (std dev of inter-sample intervals) in seconds
pub jitter_sec: f64,
/// Dropout rate (fraction of expected samples that were missing)
pub dropout_rate: f64,
/// Per-channel SNR in dB (signal = mean, noise = std dev)
pub snr_db: Vec<f64>,
/// Total samples received
pub total_samples: u64,
/// Total dropouts
pub total_dropouts: u64,
}
impl SignalQuality {
pub fn new(srate: f64, n_channels: usize) -> Self {
SignalQuality {
srate,
intervals: VecDeque::with_capacity(2048),
window: 2000,
last_ts: 0.0,
total_samples: 0,
total_dropouts: 0,
ch_sum: vec![0.0; n_channels],
ch_sum2: vec![0.0; n_channels],
ch_count: 0,
}
}
/// Feed a sample timestamp + channel values.
pub fn update(&mut self, timestamp: f64, channel_values: &[f64]) {
self.total_samples += 1;
// Track inter-sample intervals
if self.last_ts > 0.0 && timestamp > self.last_ts {
let dt = timestamp - self.last_ts;
if self.intervals.len() >= self.window {
self.intervals.pop_front();
}
self.intervals.push_back(dt);
// Detect dropout
if self.srate > 0.0 {
let expected = 1.0 / self.srate;
if dt > expected * 1.5 {
let missed = (dt / expected).round() as u64;
self.total_dropouts += missed.saturating_sub(1);
}
}
}
self.last_ts = timestamp;
// Track channel statistics (for SNR)
for (i, &v) in channel_values.iter().enumerate() {
if i < self.ch_sum.len() {
self.ch_sum[i] += v;
self.ch_sum2[i] += v * v;
}
}
self.ch_count += 1;
}
/// Compute a snapshot of current quality metrics.
pub fn snapshot(&self) -> QualitySnapshot {
let n = self.intervals.len();
if n < 2 {
return QualitySnapshot {
total_samples: self.total_samples,
total_dropouts: self.total_dropouts,
..Default::default()
};
}
// Mean interval → effective sample rate
let sum: f64 = self.intervals.iter().sum();
let mean_dt = sum / n as f64;
let effective_srate = if mean_dt > 0.0 { 1.0 / mean_dt } else { 0.0 };
// Jitter = std dev of intervals
let var: f64 = self
.intervals
.iter()
.map(|&dt| (dt - mean_dt).powi(2))
.sum::<f64>()
/ n as f64;
let jitter_sec = var.sqrt();
// Dropout rate
let dropout_rate = if self.total_samples > 0 {
self.total_dropouts as f64 / (self.total_samples + self.total_dropouts) as f64
} else {
0.0
};
// Per-channel SNR
let snr_db = if self.ch_count > 1 {
self.ch_sum
.iter()
.zip(self.ch_sum2.iter())
.map(|(&s, &s2)| {
let mean = s / self.ch_count as f64;
let var = (s2 / self.ch_count as f64) - mean * mean;
let std = var.abs().sqrt();
if std > 1e-15 {
20.0 * (mean.abs() / std).log10()
} else {
f64::INFINITY
}
})
.collect()
} else {
vec![]
};
QualitySnapshot {
effective_srate,
jitter_sec,
dropout_rate,
snr_db,
total_samples: self.total_samples,
total_dropouts: self.total_dropouts,
}
}
/// Reset all statistics.
pub fn reset(&mut self) {
self.intervals.clear();
self.last_ts = 0.0;
self.total_samples = 0;
self.total_dropouts = 0;
for v in &mut self.ch_sum {
*v = 0.0;
}
for v in &mut self.ch_sum2 {
*v = 0.0;
}
self.ch_count = 0;
}
}