use anyhow::{Context, Result};
use std::fs::File;
use std::io::{BufWriter, Write};
use std::path::{Path, PathBuf};
use super::{mat5::*, RawSet};
pub fn write_raw<P: AsRef<Path>>(
raw: &RawSet,
data: &ndarray::Array2<f32>,
set_path: P,
) -> Result<()> {
let set_path = set_path.as_ref().to_path_buf();
if set_path.extension().and_then(|s| s.to_str()) != Some("set") {
anyhow::bail!(
"write_raw: path must end in .set (got {})",
set_path.display()
);
}
let fdt_path: PathBuf = set_path.with_extension("fdt");
let (n_ch, n_t) = (data.nrows(), data.ncols());
let expected_t = raw.n_samples * raw.n_trials;
if n_ch != raw.n_chan {
anyhow::bail!(
"write_raw: data has {n_ch} channels, RawSet says {}",
raw.n_chan
);
}
if n_t != expected_t {
anyhow::bail!(
"write_raw: data has {n_t} time-points, expected n_samples × n_trials = {} × {} = {}",
raw.n_samples,
raw.n_trials,
expected_t,
);
}
{
let f =
File::create(&fdt_path).with_context(|| format!("create {}", fdt_path.display()))?;
let mut w = BufWriter::new(f);
for t in 0..n_t {
for c in 0..n_ch {
w.write_all(&data[[c, t]].to_le_bytes())?;
}
}
w.flush()?;
}
let fdt_name = fdt_path
.file_name()
.and_then(|s| s.to_str())
.unwrap_or("data.fdt")
.to_string();
let mut writer = ElementWriter::new();
writer.write_header();
let mut eeg = StructBuilder::new();
eeg.set_str(
"setname",
&raw.set_path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("eeg"),
);
eeg.set_str(
"filename",
set_path
.file_name()
.and_then(|s| s.to_str())
.unwrap_or("out.set"),
);
eeg.set_str(
"filepath",
set_path
.parent()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_default()
.as_str(),
);
eeg.set_scalar("nbchan", raw.n_chan as f64);
eeg.set_scalar("pnts", raw.n_samples as f64);
eeg.set_scalar("trials", raw.n_trials as f64);
eeg.set_scalar("srate", raw.sfreq);
eeg.set_scalar("xmin", 0.0);
eeg.set_scalar("xmax", (raw.n_samples - 1) as f64 / raw.sfreq);
eeg.set_str("data", &fdt_name);
eeg.set_str("ref", "average");
eeg.set_chanlocs(&raw.channels);
writer.write_top_level_struct("EEG", &eeg);
let f = File::create(&set_path).with_context(|| format!("create {}", set_path.display()))?;
let mut w = BufWriter::new(f);
w.write_all(&writer.bytes)?;
w.flush()?;
Ok(())
}
struct ElementWriter {
bytes: Vec<u8>,
}
impl ElementWriter {
fn new() -> Self {
Self {
bytes: Vec::with_capacity(64 * 1024),
}
}
fn write_header(&mut self) {
let mut hdr = [0u8; 128];
let desc = b"MATLAB 5.0 MAT-file, written by exg (Rust) - little-endian, uncompressed";
hdr[..desc.len()].copy_from_slice(desc);
for b in &mut hdr[desc.len()..116] {
*b = b' ';
}
hdr[124] = 0x00;
hdr[125] = 0x01;
hdr[126] = b'I';
hdr[127] = b'M';
self.bytes.extend_from_slice(&hdr);
}
fn write_top_level_struct(&mut self, name: &str, st: &StructBuilder) {
let body = build_struct_body(name, st);
self.write_normal_element(MI_MATRIX, &body);
}
fn write_normal_element(&mut self, ty: u32, body: &[u8]) {
self.bytes.extend_from_slice(&ty.to_le_bytes());
self.bytes
.extend_from_slice(&(body.len() as u32).to_le_bytes());
self.bytes.extend_from_slice(body);
align8(&mut self.bytes);
}
}
fn align8(buf: &mut Vec<u8>) {
while buf.len() % 8 != 0 {
buf.push(0);
}
}
fn emit_subelement(out: &mut Vec<u8>, ty: u32, body: &[u8]) {
out.extend_from_slice(&ty.to_le_bytes());
out.extend_from_slice(&(body.len() as u32).to_le_bytes());
out.extend_from_slice(body);
align8(out);
}
fn build_struct_body(name: &str, st: &StructBuilder) -> Vec<u8> {
let mut body = Vec::new();
let mut af = Vec::with_capacity(8);
af.extend_from_slice(&(MX_STRUCT_CLASS as u32).to_le_bytes());
af.extend_from_slice(&0u32.to_le_bytes());
emit_subelement(&mut body, MI_UINT32, &af);
let mut dims = Vec::with_capacity(8);
dims.extend_from_slice(&1i32.to_le_bytes());
dims.extend_from_slice(&1i32.to_le_bytes());
emit_subelement(&mut body, MI_INT32, &dims);
emit_subelement(&mut body, MI_INT8, name.as_bytes());
const FIELD_LEN: usize = 32;
emit_subelement(&mut body, MI_INT32, &(FIELD_LEN as i32).to_le_bytes());
let mut names_block = Vec::with_capacity(st.fields.len() * FIELD_LEN);
for (name, _) in &st.fields {
let mut bucket = vec![0u8; FIELD_LEN];
let bytes = name.as_bytes();
let n = bytes.len().min(FIELD_LEN - 1);
bucket[..n].copy_from_slice(&bytes[..n]);
names_block.extend_from_slice(&bucket);
}
emit_subelement(&mut body, MI_INT8, &names_block);
for (_name, val) in &st.fields {
let inner_body = build_value_body(val);
emit_subelement(&mut body, MI_MATRIX, &inner_body);
}
body
}
fn build_value_body(val: &MatLike) -> Vec<u8> {
let mut body = Vec::new();
match val {
MatLike::Empty => {
let mut af = Vec::with_capacity(8);
af.extend_from_slice(&(MX_DOUBLE_CLASS as u32).to_le_bytes());
af.extend_from_slice(&0u32.to_le_bytes());
emit_subelement(&mut body, MI_UINT32, &af);
let mut dims = Vec::with_capacity(8);
dims.extend_from_slice(&0i32.to_le_bytes());
dims.extend_from_slice(&0i32.to_le_bytes());
emit_subelement(&mut body, MI_INT32, &dims);
emit_subelement(&mut body, MI_INT8, &[]);
emit_subelement(&mut body, MI_DOUBLE, &[]);
}
MatLike::Scalar(x) => {
let mut af = Vec::with_capacity(8);
af.extend_from_slice(&(MX_DOUBLE_CLASS as u32).to_le_bytes());
af.extend_from_slice(&0u32.to_le_bytes());
emit_subelement(&mut body, MI_UINT32, &af);
let mut dims = Vec::with_capacity(8);
dims.extend_from_slice(&1i32.to_le_bytes());
dims.extend_from_slice(&1i32.to_le_bytes());
emit_subelement(&mut body, MI_INT32, &dims);
emit_subelement(&mut body, MI_INT8, &[]);
emit_subelement(&mut body, MI_DOUBLE, &x.to_le_bytes());
}
MatLike::Str(s) => {
let mut af = Vec::with_capacity(8);
af.extend_from_slice(&(MX_CHAR_CLASS as u32).to_le_bytes());
af.extend_from_slice(&0u32.to_le_bytes());
emit_subelement(&mut body, MI_UINT32, &af);
let mut dims = Vec::with_capacity(8);
dims.extend_from_slice(&1i32.to_le_bytes());
dims.extend_from_slice(&(s.len() as i32).to_le_bytes());
emit_subelement(&mut body, MI_INT32, &dims);
emit_subelement(&mut body, MI_INT8, &[]);
emit_subelement(&mut body, MI_UTF8, s.as_bytes());
}
MatLike::StructArray { dims, elems } => {
let mut af = Vec::with_capacity(8);
af.extend_from_slice(&(MX_STRUCT_CLASS as u32).to_le_bytes());
af.extend_from_slice(&0u32.to_le_bytes());
emit_subelement(&mut body, MI_UINT32, &af);
let mut dims_buf = Vec::with_capacity(4 * dims.len());
for d in dims {
dims_buf.extend_from_slice(&(*d as i32).to_le_bytes());
}
emit_subelement(&mut body, MI_INT32, &dims_buf);
emit_subelement(&mut body, MI_INT8, &[]);
const FIELD_LEN: usize = 32;
emit_subelement(&mut body, MI_INT32, &(FIELD_LEN as i32).to_le_bytes());
let field_names: Vec<&str> = elems
.first()
.map(|e| e.fields.iter().map(|(n, _)| n.as_str()).collect())
.unwrap_or_default();
let mut names_block = Vec::with_capacity(field_names.len() * FIELD_LEN);
for fname in &field_names {
let mut bucket = vec![0u8; FIELD_LEN];
let bytes = fname.as_bytes();
let n = bytes.len().min(FIELD_LEN - 1);
bucket[..n].copy_from_slice(&bytes[..n]);
names_block.extend_from_slice(&bucket);
}
emit_subelement(&mut body, MI_INT8, &names_block);
for el in elems {
for fname in &field_names {
let v = el
.fields
.iter()
.find(|(n, _)| n == fname)
.map(|(_, v)| v.clone())
.unwrap_or(MatLike::Empty);
let inner = build_value_body(&v);
emit_subelement(&mut body, MI_MATRIX, &inner);
}
}
}
}
body
}
#[derive(Clone)]
enum MatLike {
Empty,
Scalar(f64),
Str(String),
StructArray {
dims: Vec<usize>,
elems: Vec<StructBuilder>,
},
}
#[derive(Clone, Default)]
struct StructBuilder {
fields: Vec<(String, MatLike)>,
}
impl StructBuilder {
fn new() -> Self {
Self::default()
}
fn set_scalar(&mut self, name: &str, x: f64) {
self.fields.push((name.to_string(), MatLike::Scalar(x)));
}
fn set_str(&mut self, name: &str, s: &str) {
self.fields
.push((name.to_string(), MatLike::Str(s.to_string())));
}
fn set_chanlocs(&mut self, chans: &[super::EeglabChannel]) {
let mut elems: Vec<StructBuilder> = Vec::with_capacity(chans.len());
for ch in chans {
let mut sb = StructBuilder::new();
sb.set_str("labels", &ch.label);
sb.set_scalar("X", ch.xyz[0] as f64);
sb.set_scalar("Y", ch.xyz[1] as f64);
sb.set_scalar("Z", ch.xyz[2] as f64);
elems.push(sb);
}
self.fields.push((
"chanlocs".to_string(),
MatLike::StructArray {
dims: vec![1, chans.len()],
elems,
},
));
}
}