use std::path::PathBuf;
use serde::{Deserialize, Serialize};
const EDF_HEADER_BLOCK: usize = 256;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SampleDtype {
I16,
I32,
I64,
}
impl SampleDtype {
pub fn width(self) -> usize {
match self {
SampleDtype::I16 => 2,
SampleDtype::I32 => 4,
SampleDtype::I64 => 8,
}
}
pub fn as_token(self) -> &'static str {
match self {
SampleDtype::I16 => "i16",
SampleDtype::I32 => "i32",
SampleDtype::I64 => "i64",
}
}
}
impl Default for SampleDtype {
fn default() -> Self {
SampleDtype::I32
}
}
pub struct ScratchDir {
pub path: PathBuf,
}
impl ScratchDir {
pub fn new(tag: &str) -> std::io::Result<Self> {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
use std::sync::atomic::{AtomicU64, Ordering};
static SEQ: AtomicU64 = AtomicU64::new(0);
let seq = SEQ.fetch_add(1, Ordering::Relaxed);
let name = format!("lqs_{}_{}_{}_{}", tag, std::process::id(), nanos, seq);
let path = std::env::temp_dir().join(name);
std::fs::create_dir_all(&path)?;
Ok(Self { path })
}
pub fn join(&self, name: &str) -> PathBuf {
self.path.join(name)
}
}
impl Drop for ScratchDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.path);
}
}
fn edf_field(out: &mut Vec<u8>, value: &str, width: usize) -> Result<(), ()> {
let bytes = value.as_bytes();
if bytes.len() > width {
return Err(());
}
out.extend_from_slice(bytes);
out.resize(out.len() + (width - bytes.len()), b' ');
Ok(())
}
fn format_edf_number(x: f64) -> String {
if x.fract() == 0.0 && x.abs() < 1e15 {
format!("{}", x as i64)
} else {
let s = format!("{x:.6}");
let trimmed = s.trim_end_matches('0').trim_end_matches('.');
trimmed.to_string()
}
}
pub fn write_edf_bytes(signal: &[Vec<i64>], fs: f64) -> Option<Vec<u8>> {
let ns = signal.len();
if ns == 0 {
return None;
}
let spr = signal[0].len();
if spr == 0 || signal.iter().any(|c| c.len() != spr) {
return None;
}
if signal
.iter()
.flat_map(|c| c.iter())
.any(|&s| s < i16::MIN as i64 || s > i16::MAX as i64)
{
return None;
}
if !fs.is_finite() || fs <= 0.0 {
return None;
}
let record_duration = spr as f64 / fs;
let dur_str = format_edf_number(record_duration);
if dur_str.len() > 8 {
return None;
}
let header_bytes = EDF_HEADER_BLOCK + ns * EDF_HEADER_BLOCK;
let header_str = header_bytes.to_string();
if header_str.len() > 8 || ns.to_string().len() > 4 || spr.to_string().len() > 8 {
return None;
}
let mut buf = Vec::with_capacity(header_bytes + ns * spr * 2);
edf_field(&mut buf, "0", 8).ok()?; edf_field(&mut buf, "OpenECS X X X", 80).ok()?; edf_field(&mut buf, "Startdate X", 80).ok()?; edf_field(&mut buf, "01.01.26", 8).ok()?; edf_field(&mut buf, "00.00.00", 8).ok()?; edf_field(&mut buf, &header_str, 8).ok()?; edf_field(&mut buf, "", 44).ok()?; edf_field(&mut buf, "1", 8).ok()?; edf_field(&mut buf, &dur_str, 8).ok()?; edf_field(&mut buf, &ns.to_string(), 4).ok()?;
for i in 0..ns {
edf_field(&mut buf, &format!("ch{i}"), 16).ok()?; }
for _ in 0..ns {
edf_field(&mut buf, "AgAgCl", 80).ok()?; }
for _ in 0..ns {
edf_field(&mut buf, "uV", 8).ok()?; }
for _ in 0..ns {
edf_field(&mut buf, "-32768", 8).ok()?; }
for _ in 0..ns {
edf_field(&mut buf, "32767", 8).ok()?; }
for _ in 0..ns {
edf_field(&mut buf, "-32768", 8).ok()?; }
for _ in 0..ns {
edf_field(&mut buf, "32767", 8).ok()?; }
for _ in 0..ns {
edf_field(&mut buf, "", 80).ok()?; }
for _ in 0..ns {
edf_field(&mut buf, &spr.to_string(), 8).ok()?; }
for _ in 0..ns {
edf_field(&mut buf, "", 32).ok()?; }
debug_assert_eq!(buf.len(), header_bytes, "EDF header block size mismatch");
for chan in signal {
for &s in chan {
buf.extend_from_slice(&(s as i16).to_le_bytes());
}
}
Some(buf)
}
pub fn reshape_channel_major(
raw: &[u8],
n_chan: usize,
per_chan: usize,
dtype: SampleDtype,
) -> Option<Vec<Vec<i64>>> {
let width = dtype.width();
let total = n_chan.checked_mul(per_chan)?.checked_mul(width)?;
if raw.len() != total {
return None;
}
let mut out = Vec::with_capacity(n_chan);
let mut pos = 0usize;
for _ in 0..n_chan {
let mut chan = Vec::with_capacity(per_chan);
for _ in 0..per_chan {
let v = match dtype {
SampleDtype::I16 => {
i16::from_le_bytes([raw[pos], raw[pos + 1]]) as i64
}
SampleDtype::I32 => {
i32::from_le_bytes([raw[pos], raw[pos + 1], raw[pos + 2], raw[pos + 3]])
as i64
}
SampleDtype::I64 => i64::from_le_bytes([
raw[pos],
raw[pos + 1],
raw[pos + 2],
raw[pos + 3],
raw[pos + 4],
raw[pos + 5],
raw[pos + 6],
raw[pos + 7],
]),
};
chan.push(v);
pos += width;
}
out.push(chan);
}
Some(out)
}
#[cfg(test)]
mod tests {
use super::*;
fn fixture() -> Vec<Vec<i64>> {
vec![vec![0, 1, -1, 1000, -1000], vec![5, 6, 7, 8, 9]]
}
#[test]
fn scratchdir_is_unique_and_cleans_up() {
let a = ScratchDir::new("t").expect("scratch a");
let b = ScratchDir::new("t").expect("scratch b");
assert_ne!(a.path, b.path, "two scratch dirs must not collide");
let p = a.path.clone();
assert!(p.is_dir());
drop(a);
assert!(!p.exists(), "scratch dir removed on drop");
drop(b);
}
#[test]
fn write_edf_rejects_unsupported_shapes() {
assert!(write_edf_bytes(&[], 256.0).is_none());
assert!(write_edf_bytes(&[vec![]], 256.0).is_none());
assert!(write_edf_bytes(&[vec![1, 2], vec![1]], 256.0).is_none());
assert!(write_edf_bytes(&[vec![i64::MAX]], 256.0).is_none());
assert!(write_edf_bytes(&[vec![1, 2]], 0.0).is_none());
let edf = write_edf_bytes(&fixture(), 256.0).expect("valid fixture -> EDF");
let ns = 2usize;
let spr = 5usize;
assert_eq!(edf.len(), EDF_HEADER_BLOCK * (1 + ns) + ns * spr * 2);
assert_eq!(&edf[..8], b"0 ", "EDF version field");
}
#[test]
fn reshape_round_trips_each_dtype() {
for dt in [SampleDtype::I16, SampleDtype::I32, SampleDtype::I64] {
let sig = vec![vec![0i64, 1, -1, 100], vec![-100, 2, 3, 4]];
let mut raw = Vec::new();
for chan in &sig {
for &s in chan {
match dt {
SampleDtype::I16 => raw.extend_from_slice(&(s as i16).to_le_bytes()),
SampleDtype::I32 => raw.extend_from_slice(&(s as i32).to_le_bytes()),
SampleDtype::I64 => raw.extend_from_slice(&s.to_le_bytes()),
}
}
}
let back = reshape_channel_major(&raw, 2, 4, dt).expect("valid stream");
assert_eq!(back, sig, "round trip for {dt:?}");
}
}
#[test]
fn reshape_rejects_wrong_length() {
let raw = vec![0u8; 30];
assert!(reshape_channel_major(&raw, 2, 4, SampleDtype::I32).is_none());
assert!(reshape_channel_major(&[], usize::MAX, usize::MAX, SampleDtype::I64).is_none());
}
#[test]
fn dtype_width_and_token() {
assert_eq!(SampleDtype::I16.width(), 2);
assert_eq!(SampleDtype::I32.width(), 4);
assert_eq!(SampleDtype::I64.width(), 8);
assert_eq!(SampleDtype::default(), SampleDtype::I32);
assert_eq!(SampleDtype::I16.as_token(), "i16");
}
}