use alloc::{string::String, vec::Vec};
use core::fmt;
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ChannelData {
Raw(Vec<i16>),
Float(Vec<f64>),
Scaled {
raw: Vec<i16>,
scale: f64,
offset: f64,
},
}
impl ChannelData {
pub const fn len(&self) -> usize {
match self {
Self::Raw(v) => v.len(),
Self::Float(v) => v.len(),
Self::Scaled { raw, .. } => raw.len(),
}
}
pub const fn is_empty(&self) -> bool {
self.len() == 0
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ChannelMetadata {
pub name: String,
pub units: String,
pub description: String,
pub frequency_divider: u16,
pub amplitude_scale: f64,
pub amplitude_offset: f64,
pub display_order: u16,
pub sample_count: u32,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Channel {
pub name: String,
pub units: String,
pub samples_per_second: f64,
pub frequency_divider: u16,
pub data: ChannelData,
pub point_count: usize,
}
impl Channel {
pub fn scaled_samples(&self) -> Vec<f64> {
match &self.data {
ChannelData::Raw(raw) => raw.iter().map(|&v| f64::from(v)).collect(),
ChannelData::Float(f) => f.clone(),
ChannelData::Scaled { raw, scale, offset } => {
raw.iter().map(|&v| f64::from(v) * scale + offset).collect()
}
}
}
pub fn upsampled(&self, base_rate: f64) -> Vec<f64> {
let src = self.scaled_samples();
let factor_f = base_rate / self.samples_per_second;
let diff = factor_f - 1.0_f64;
if diff > -f64::EPSILON && diff < f64::EPSILON {
return src;
}
#[expect(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "factor_f = base_rate / samples_per_second; always positive for valid .acq files"
)]
let factor = (factor_f + 0.5) as usize;
if factor == 0 {
return src;
}
let mut out = Vec::with_capacity(src.len() * factor);
for [a, b] in src.array_windows::<2>() {
for i in 0..factor {
#[expect(
clippy::cast_precision_loss,
reason = "sample indices are small frame counts; precision loss is irrelevant for resampling"
)]
out.push(a + (b - a) * (i as f64 / factor as f64));
}
}
if let Some(&last) = src.last() {
for _ in 0..factor {
out.push(last);
}
}
out
}
}
impl fmt::Display for Channel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"Channel({:?}, {} samples, {} Hz)",
self.name, self.point_count, self.samples_per_second
)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_channel(divider: u16, base_rate: f64, samples: Vec<i16>) -> Channel {
let count = samples.len();
Channel {
name: String::from("test"),
units: String::from("mV"),
samples_per_second: base_rate / f64::from(divider),
frequency_divider: divider,
data: ChannelData::Raw(samples),
point_count: count,
}
}
#[test]
fn channel_frequency_divider_halves_rate() {
let ch = make_channel(2, 1000.0, alloc::vec![0, 1, 2]);
assert!((ch.samples_per_second - 500.0).abs() < f64::EPSILON);
}
#[test]
fn scaled_samples_raw_cast() {
let ch = make_channel(1, 1000.0, alloc::vec![1, 2, 3]);
let scaled = ch.scaled_samples();
assert_eq!(scaled, alloc::vec![1.0, 2.0, 3.0]);
}
#[test]
fn scaled_samples_with_coefficients() {
let ch = Channel {
name: String::from("ecg"),
units: String::from("mV"),
samples_per_second: 1000.0,
frequency_divider: 1,
data: ChannelData::Scaled {
raw: alloc::vec![0, 100],
scale: 0.01,
offset: -0.5,
},
point_count: 2,
};
let scaled = ch.scaled_samples();
assert!((scaled.first().copied().unwrap_or(f64::NAN) - (-0.5)).abs() < 1e-10);
assert!((scaled.get(1).copied().unwrap_or(f64::NAN) - 0.5).abs() < 1e-10);
}
#[test]
fn upsampled_identity_at_same_rate() {
let ch = make_channel(1, 1000.0, alloc::vec![10, 20, 30]);
let up = ch.upsampled(1000.0);
assert_eq!(up, alloc::vec![10.0, 20.0, 30.0]);
}
#[test]
fn upsampled_doubles_samples_at_2x_rate() {
let ch = make_channel(2, 1000.0, alloc::vec![0, 10]);
let up = ch.upsampled(1000.0);
assert_eq!(up.len(), 4);
assert!((up.first().copied().unwrap_or(f64::NAN) - 0.0).abs() < 1e-10);
assert!((up.get(1).copied().unwrap_or(f64::NAN) - 5.0).abs() < 1e-10);
assert!((up.get(2).copied().unwrap_or(f64::NAN) - 10.0).abs() < 1e-10);
assert!((up.get(3).copied().unwrap_or(f64::NAN) - 10.0).abs() < 1e-10);
}
}