use std::fs::File;
use std::io::{BufWriter, Write};
use std::sync::Mutex;
#[repr(u16)]
#[derive(Clone, Copy)]
enum ChunkTag {
FileHeader = 1,
StreamHeader = 2,
Samples = 3,
ClockOffset = 4,
Boundary = 5,
StreamFooter = 6,
}
pub struct XdfWriter {
inner: Mutex<BufWriter<File>>,
}
impl XdfWriter {
pub fn new(path: &str) -> anyhow::Result<Self> {
let file = File::create(path)?;
let mut w = BufWriter::new(file);
w.write_all(b"XDF:")?;
let content = "<?xml version=\"1.0\"?><info><version>1.0</version></info>";
write_chunk(&mut w, ChunkTag::FileHeader, None, content.as_bytes())?;
w.flush()?;
Ok(XdfWriter {
inner: Mutex::new(w),
})
}
pub fn write_stream_header(&self, stream_id: u32, xml_content: &str) -> anyhow::Result<()> {
let mut w = self.inner.lock().unwrap();
write_chunk(
&mut *w,
ChunkTag::StreamHeader,
Some(stream_id),
xml_content.as_bytes(),
)?;
w.flush()?;
Ok(())
}
pub fn write_stream_footer(&self, stream_id: u32, xml_content: &str) -> anyhow::Result<()> {
let mut w = self.inner.lock().unwrap();
write_chunk(
&mut *w,
ChunkTag::StreamFooter,
Some(stream_id),
xml_content.as_bytes(),
)?;
w.flush()?;
Ok(())
}
pub fn write_clock_offset(
&self,
stream_id: u32,
collection_time: f64,
offset_value: f64,
) -> anyhow::Result<()> {
let mut buf = Vec::with_capacity(16);
buf.extend_from_slice(&collection_time.to_le_bytes());
buf.extend_from_slice(&offset_value.to_le_bytes());
let mut w = self.inner.lock().unwrap();
write_chunk(&mut *w, ChunkTag::ClockOffset, Some(stream_id), &buf)?;
Ok(())
}
pub fn write_boundary(&self) -> anyhow::Result<()> {
let boundary_uuid: [u8; 16] = [
0x43, 0xA5, 0x46, 0xDC, 0xCB, 0xF5, 0x41, 0x0F, 0xB3, 0x0E, 0xD5, 0x46, 0x73, 0x83,
0xCB, 0xE4,
];
let mut w = self.inner.lock().unwrap();
write_chunk(&mut *w, ChunkTag::Boundary, None, &boundary_uuid)?;
Ok(())
}
pub fn write_samples_numeric<T: NumericSample>(
&self,
stream_id: u32,
timestamps: &[f64],
data: &[T],
n_channels: u32,
) -> anyhow::Result<()> {
if timestamps.is_empty() {
return Ok(());
}
let n_samples = timestamps.len();
assert_eq!(data.len(), n_samples * n_channels as usize);
let mut payload = Vec::with_capacity(n_samples * (9 + n_channels as usize * T::SIZE));
write_varlen_int(&mut payload, n_samples as u64);
for (i, &ts) in timestamps.iter().enumerate() {
if ts == 0.0 {
payload.push(0);
} else {
payload.push(8);
payload.extend_from_slice(&ts.to_le_bytes());
}
let offset = i * n_channels as usize;
for j in 0..n_channels as usize {
data[offset + j].write_le(&mut payload);
}
}
let mut w = self.inner.lock().unwrap();
write_chunk(&mut *w, ChunkTag::Samples, Some(stream_id), &payload)?;
Ok(())
}
#[allow(dead_code)]
pub fn write_samples_string(
&self,
stream_id: u32,
timestamps: &[f64],
data: &[String],
n_channels: u32,
) -> anyhow::Result<()> {
if timestamps.is_empty() {
return Ok(());
}
let n_samples = timestamps.len();
assert_eq!(data.len(), n_samples * n_channels as usize);
let mut payload = Vec::with_capacity(256);
write_varlen_int(&mut payload, n_samples as u64);
for (i, &ts) in timestamps.iter().enumerate() {
if ts == 0.0 {
payload.push(0);
} else {
payload.push(8);
payload.extend_from_slice(&ts.to_le_bytes());
}
let offset = i * n_channels as usize;
for j in 0..n_channels as usize {
let s = &data[offset + j];
write_varlen_int(&mut payload, s.len() as u64);
payload.extend_from_slice(s.as_bytes());
}
}
let mut w = self.inner.lock().unwrap();
write_chunk(&mut *w, ChunkTag::Samples, Some(stream_id), &payload)?;
Ok(())
}
#[allow(dead_code)]
pub fn file_size(&self) -> u64 {
let w = self.inner.lock().unwrap();
w.get_ref().metadata().map(|m| m.len()).unwrap_or(0)
}
}
fn write_chunk<W: Write>(
w: &mut W,
tag: ChunkTag,
stream_id: Option<u32>,
content: &[u8],
) -> std::io::Result<()> {
let length = 2 + if stream_id.is_some() { 4 } else { 0 } + content.len();
write_chunk_length(w, length as u64)?;
w.write_all(&(tag as u16).to_le_bytes())?;
if let Some(sid) = stream_id {
w.write_all(&sid.to_le_bytes())?;
}
w.write_all(content)?;
Ok(())
}
fn write_chunk_length<W: Write>(w: &mut W, length: u64) -> std::io::Result<()> {
if length < 256 {
w.write_all(&[1])?; w.write_all(&[length as u8])?;
} else if length < (1u64 << 32) {
w.write_all(&[4])?;
w.write_all(&(length as u32).to_le_bytes())?;
} else {
w.write_all(&[8])?;
w.write_all(&length.to_le_bytes())?;
}
Ok(())
}
fn write_varlen_int(buf: &mut Vec<u8>, val: u64) {
if val < 256 {
buf.push(1);
buf.push(val as u8);
} else if val < (1u64 << 32) {
buf.push(4);
buf.extend_from_slice(&(val as u32).to_le_bytes());
} else {
buf.push(8);
buf.extend_from_slice(&val.to_le_bytes());
}
}
pub trait NumericSample: Copy {
const SIZE: usize;
fn write_le(&self, buf: &mut Vec<u8>);
}
impl NumericSample for f32 {
const SIZE: usize = 4;
fn write_le(&self, buf: &mut Vec<u8>) {
buf.extend_from_slice(&self.to_le_bytes());
}
}
impl NumericSample for f64 {
const SIZE: usize = 8;
fn write_le(&self, buf: &mut Vec<u8>) {
buf.extend_from_slice(&self.to_le_bytes());
}
}
impl NumericSample for i32 {
const SIZE: usize = 4;
fn write_le(&self, buf: &mut Vec<u8>) {
buf.extend_from_slice(&self.to_le_bytes());
}
}
impl NumericSample for i16 {
const SIZE: usize = 2;
fn write_le(&self, buf: &mut Vec<u8>) {
buf.extend_from_slice(&self.to_le_bytes());
}
}
impl NumericSample for i64 {
const SIZE: usize = 8;
fn write_le(&self, buf: &mut Vec<u8>) {
buf.extend_from_slice(&self.to_le_bytes());
}
}
impl NumericSample for i8 {
const SIZE: usize = 1;
fn write_le(&self, buf: &mut Vec<u8>) {
buf.push(*self as u8);
}
}