use std::io::Write;
use std::path::Path;
use crate::blocks::conversion::{Conversion, TableEntry};
use crate::blocks::ChannelType;
use crate::error::{Mf4Error, Result};
use crate::model::SignalValues;
const ID_SIZE: u64 = 64;
const HD_SIZE: u64 = 104;
const FH_SIZE: u64 = 56;
const DG_SIZE: u64 = 64;
const CG_SIZE: u64 = 104;
const CN_SIZE: u64 = 160;
const DT_HEADER_SIZE: u64 = 24;
const SD_HEADER_SIZE: u64 = 24;
const CC_HEADER_SIZE: u64 = 24;
const HL_SIZE: u64 = 40;
const DL_SIZE: u64 = 56;
const DZ_HEADER_SIZE: u64 = 48;
const SAMPLE_BYTES: u64 = 8;
const CN_FLAG_INVALIDATION_BIT: u32 = 0x0002;
const CN_FLAG_VLSD_OFFSET: u32 = 0x4000;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum WriteCodec {
#[default]
Deflate,
TransposedDeflate,
#[cfg(feature = "lz4")]
Lz4,
#[cfg(feature = "lz4")]
TransposedLz4,
}
impl WriteCodec {
fn zip_type(self) -> u8 {
match self {
WriteCodec::Deflate => 0,
WriteCodec::TransposedDeflate => 1,
#[cfg(feature = "lz4")]
WriteCodec::Lz4 => 4,
#[cfg(feature = "lz4")]
WriteCodec::TransposedLz4 => 5,
}
}
fn is_transposed(self) -> bool {
match self {
WriteCodec::Deflate => false,
WriteCodec::TransposedDeflate => true,
#[cfg(feature = "lz4")]
WriteCodec::Lz4 => false,
#[cfg(feature = "lz4")]
WriteCodec::TransposedLz4 => true,
}
}
}
#[derive(Debug, Default)]
pub struct Mf4Writer {
start_time_ns: i64,
compress: bool,
codec: WriteCodec,
groups: Vec<WriteGroup>,
next_dg_id: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RewriteMode {
BestEffort,
Strict,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RewriteIssue {
pub location: Option<(usize, usize, usize)>,
pub name: String,
pub reason: String,
}
#[derive(Debug, Default, Clone)]
pub struct RewriteReport {
pub issues: Vec<RewriteIssue>,
}
impl RewriteReport {
pub fn is_lossless(&self) -> bool {
self.issues.is_empty()
}
fn note(
&mut self,
location: Option<(usize, usize, usize)>,
name: &str,
reason: impl Into<String>,
) {
self.issues.push(RewriteIssue {
location,
name: name.into(),
reason: reason.into(),
});
}
}
#[derive(Debug, Default)]
pub struct WriteGroup {
times: Vec<f64>,
channels: Vec<WriteChannel>,
pub(crate) dg_id: usize,
}
#[derive(Debug, Clone)]
pub struct WriteChannel {
pub(crate) name: String,
pub(crate) unit: String,
pub(crate) comment: String,
pub(crate) values: SignalValues,
pub(crate) valid: Option<Vec<bool>>,
pub(crate) conversion: Option<Conversion>,
pub(crate) format: SampleFormat,
pub(crate) is_vlsd: bool,
pub(crate) array_shape: Option<Vec<u64>>,
}
impl WriteChannel {
pub fn name(&self) -> &str {
&self.name
}
pub fn set_name(&mut self, name: impl Into<String>) {
self.name = name.into();
}
pub fn unit(&self) -> &str {
&self.unit
}
pub fn set_unit(&mut self, unit: impl Into<String>) {
self.unit = unit.into();
}
pub fn comment(&self) -> &str {
&self.comment
}
pub fn set_comment(&mut self, comment: impl Into<String>) {
self.comment = comment.into();
}
pub fn conversion(&self) -> Option<&Conversion> {
self.conversion.as_ref()
}
pub fn set_conversion(&mut self, conversion: Option<Conversion>) -> Result<()> {
if let Some(c) = &conversion {
if !c.is_identity() {
CcPlan::of(c, &self.name)?;
}
}
self.conversion = conversion;
Ok(())
}
pub fn is_vlsd(&self) -> bool {
self.is_vlsd
}
pub fn set_vlsd(&mut self, is_vlsd: bool) -> Result<()> {
self.format = SampleFormat::of(&self.values, &self.name, is_vlsd)?;
self.is_vlsd = is_vlsd;
Ok(())
}
pub fn is_array(&self) -> bool {
self.array_shape.is_some()
}
pub fn array_shape(&self) -> Option<&[u64]> {
self.array_shape.as_deref()
}
pub fn set_array_shape(&mut self, shape: Option<Vec<u64>>) -> Result<()> {
if let Some(s) = &shape {
if s.is_empty() || s.contains(&0) {
return Err(Mf4Error::write_error(format!(
"channel '{}' cannot be given invalid array shape {s:?}; dimensions must be non-empty and non-zero",
self.name
)));
}
let total: u64 = s
.iter()
.copied()
.try_fold(1u64, |acc, d| acc.checked_mul(d))
.ok_or_else(|| {
Mf4Error::write_error(format!(
"channel '{}' shape {s:?} overflows element count",
self.name
))
})?;
if let SignalValues::Array {
elements_per_sample,
..
} = &self.values
{
if *elements_per_sample as u64 != total {
return Err(Mf4Error::write_error(format!(
"channel '{}' shape {s:?} implies {total} elements per sample, but channel values has {elements_per_sample}",
self.name
)));
}
} else {
return Err(Mf4Error::write_error(format!(
"channel '{}' holds {} samples, which cannot have an array shape; use SignalValues::Array",
self.name,
self.values.kind()
)));
}
}
self.array_shape = shape;
Ok(())
}
pub fn values(&self) -> &SignalValues {
&self.values
}
pub fn set_values(&mut self, values: SignalValues) -> Result<()> {
if values.len() != self.values.len() {
return Err(Mf4Error::write_error(format!(
"new values length ({}) does not match current channel length ({})",
values.len(),
self.values.len()
)));
}
let format = SampleFormat::of(&values, &self.name, self.is_vlsd)?;
if let Some(shape) = &self.array_shape {
let total: u64 = shape.iter().copied().product();
if format.elements_per_sample as u64 != total {
return Err(Mf4Error::write_error(format!(
"new values elements per sample ({}) does not match channel array shape {:?} ({total})",
format.elements_per_sample,
shape
)));
}
}
self.format = format;
self.values = values;
Ok(())
}
pub fn valid(&self) -> Option<&[bool]> {
self.valid.as_deref()
}
pub fn set_valid(&mut self, valid: Option<Vec<bool>>) -> Result<()> {
if let Some(v) = &valid {
if v.len() != self.values.len() {
return Err(Mf4Error::write_error(format!(
"validity flags length ({}) does not match channel values length ({})",
v.len(),
self.values.len()
)));
}
}
self.valid = valid;
Ok(())
}
pub fn len(&self) -> usize {
self.values.len()
}
pub fn is_empty(&self) -> bool {
self.values.is_empty()
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct SampleFormat {
pub(crate) data_type: u8,
pub(crate) width: u32,
pub(crate) record_width: u32,
pub(crate) is_vlsd: bool,
pub(crate) elements_per_sample: usize,
}
impl SampleFormat {
fn of(values: &SignalValues, name: &str, is_vlsd: bool) -> Result<Self> {
if is_vlsd {
let data_type = match values {
SignalValues::Str(_) => 7,
SignalValues::VarBytes { .. } | SignalValues::Bytes { .. } => 10,
other => {
return Err(Mf4Error::write_error(format!(
"channel '{name}' holds {} samples, which cannot be written as variable-length; \
VLSD channels must hold strings (Str) or byte arrays (VarBytes, Bytes)",
other.kind()
)))
}
};
return Ok(SampleFormat {
data_type,
width: 8,
record_width: 8,
is_vlsd: true,
elements_per_sample: 1,
});
}
let (data_type, width, record_width, elements_per_sample) = match values {
SignalValues::U8(_) => (0, 1, 1, 1),
SignalValues::U16(_) => (0, 2, 2, 1),
SignalValues::U32(_) => (0, 4, 4, 1),
SignalValues::U64(_) => (0, 8, 8, 1),
SignalValues::I8(_) => (2, 1, 1, 1),
SignalValues::I16(_) => (2, 2, 2, 1),
SignalValues::I32(_) => (2, 4, 4, 1),
SignalValues::I64(_) => (2, 8, 8, 1),
SignalValues::F32(_) => (4, 4, 4, 1),
SignalValues::F64(_) => (4, 8, 8, 1),
SignalValues::Str(v) => {
let width = v.iter().map(|s| s.len()).max().unwrap_or(0).max(1);
let w = u32::try_from(width).map_err(|_| too_wide(name, width))?;
(7, w, w, 1)
}
SignalValues::Bytes { width, .. } => {
let w = u32::try_from((*width).max(1)).map_err(|_| too_wide(name, *width))?;
(10, w, w, 1)
}
SignalValues::Array {
elements_per_sample,
..
} => {
if *elements_per_sample == 0 {
return Err(Mf4Error::write_error(format!(
"channel '{name}' has an empty array shape (0 elements per sample)"
)));
}
let elem_width = 8u32;
let rec_width = u32::try_from(*elements_per_sample * 8)
.map_err(|_| too_wide(name, *elements_per_sample * 8))?;
(4, elem_width, rec_width, *elements_per_sample)
}
other => {
return Err(Mf4Error::write_error(format!(
"channel '{name}' holds {} samples, which this writer has no record \
layout for; it writes integers, floats, fixed-length strings, \
fixed-width byte runs, and fixed-size arrays (use add_channel_vlsd for variable-length samples)",
other.kind()
)))
}
};
Ok(SampleFormat {
data_type,
width,
record_width,
is_vlsd: false,
elements_per_sample,
})
}
fn encode(&self, values: &SignalValues, index: usize, out: &mut Vec<u8>) {
let width = self.record_width as usize;
let before = out.len();
match values {
SignalValues::U8(v) => out.push(v[index]),
SignalValues::U16(v) => out.extend_from_slice(&v[index].to_le_bytes()),
SignalValues::U32(v) => out.extend_from_slice(&v[index].to_le_bytes()),
SignalValues::U64(v) => out.extend_from_slice(&v[index].to_le_bytes()),
SignalValues::I8(v) => out.extend_from_slice(&v[index].to_le_bytes()),
SignalValues::I16(v) => out.extend_from_slice(&v[index].to_le_bytes()),
SignalValues::I32(v) => out.extend_from_slice(&v[index].to_le_bytes()),
SignalValues::I64(v) => out.extend_from_slice(&v[index].to_le_bytes()),
SignalValues::F32(v) => out.extend_from_slice(&v[index].to_le_bytes()),
SignalValues::F64(v) => out.extend_from_slice(&v[index].to_le_bytes()),
SignalValues::Str(v) => out.extend_from_slice(v[index].as_bytes()),
SignalValues::Bytes { data, width: w } => {
out.extend_from_slice(&data[index * w..(index + 1) * w])
}
SignalValues::Array {
values,
elements_per_sample,
} => {
let start = index * elements_per_sample;
let end = start + elements_per_sample;
if end <= values.len() {
for &val in &values[start..end] {
out.extend_from_slice(&val.to_le_bytes());
}
}
}
_ => {}
}
out.resize(before + width, 0);
}
}
fn vlsd_sample_bytes(values: &SignalValues, index: usize) -> &[u8] {
match values {
SignalValues::Str(v) => v[index].as_bytes(),
SignalValues::VarBytes { data, starts } => &data[starts[index]..starts[index + 1]],
SignalValues::Bytes { data, width } => &data[index * width..(index + 1) * width],
_ => &[],
}
}
fn build_sd_data(values: &SignalValues) -> (Vec<u8>, Vec<u64>) {
let n = values.len();
let mut sd_data = Vec::new();
let mut offsets = Vec::with_capacity(n);
for i in 0..n {
let sample = vlsd_sample_bytes(values, i);
let offset = sd_data.len() as u64;
offsets.push(offset);
sd_data.extend_from_slice(&(sample.len() as u32).to_le_bytes());
sd_data.extend_from_slice(sample);
}
(sd_data, offsets)
}
fn too_wide(name: &str, width: usize) -> Mf4Error {
Mf4Error::write_error(format!(
"channel '{name}' has samples of {width} bytes, more than a record can describe"
))
}
impl Mf4Writer {
pub fn new() -> Self {
let start_time_ns = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| i64::try_from(d.as_nanos()).unwrap_or(i64::MAX))
.unwrap_or(0);
Self {
start_time_ns,
compress: false,
codec: WriteCodec::default(),
groups: Vec::new(),
next_dg_id: 0,
}
}
pub fn with_start_time_ns(start_time_ns: i64) -> Self {
Self {
start_time_ns,
compress: false,
codec: WriteCodec::default(),
groups: Vec::new(),
next_dg_id: 0,
}
}
pub fn set_compression(&mut self, on: bool) {
self.compress = on;
}
pub fn set_codec(&mut self, codec: WriteCodec) {
self.codec = codec;
}
pub fn codec(&self) -> WriteCodec {
self.codec
}
pub fn add_group(&mut self, times: &[f64]) -> Result<&mut WriteGroup> {
if times.iter().any(|t| t.is_nan()) {
return Err(Mf4Error::write_error(
"a group's time axis contains NaN, which has no place in the order records are sorted by",
));
}
let dg_id = self.next_dg_id;
self.next_dg_id += 1;
self.groups.push(WriteGroup {
times: times.to_vec(),
channels: Vec::new(),
dg_id,
});
Ok(self.groups.last_mut().expect("just pushed"))
}
pub fn add_group_in(&mut self, sibling: usize, times: &[f64]) -> Result<&mut WriteGroup> {
if sibling >= self.groups.len() {
return Err(Mf4Error::write_error(format!(
"sibling group index {sibling} is out of range (writer has {} groups)",
self.groups.len()
)));
}
if times.iter().any(|t| t.is_nan()) {
return Err(Mf4Error::write_error(
"a group's time axis contains NaN, which has no place in the order records are sorted by",
));
}
let dg_id = self.groups[sibling].dg_id;
self.groups.push(WriteGroup {
times: times.to_vec(),
channels: Vec::new(),
dg_id,
});
Ok(self.groups.last_mut().expect("just pushed"))
}
pub fn start_time_ns(&self) -> i64 {
self.start_time_ns
}
pub fn set_start_time_ns(&mut self, start_time_ns: i64) {
self.start_time_ns = start_time_ns;
}
pub fn is_compressed(&self) -> bool {
self.compress
}
pub fn groups(&self) -> &[WriteGroup] {
&self.groups
}
pub fn groups_mut(&mut self) -> &mut [WriteGroup] {
&mut self.groups
}
pub fn group(&self, index: usize) -> Option<&WriteGroup> {
self.groups.get(index)
}
pub fn group_mut(&mut self, index: usize) -> Option<&mut WriteGroup> {
self.groups.get_mut(index)
}
pub fn remove_group(&mut self, index: usize) -> WriteGroup {
self.groups.remove(index)
}
pub fn retain_groups<F: FnMut(&WriteGroup) -> bool>(&mut self, mut f: F) {
self.groups.retain(|g| f(g));
}
pub fn from_file(file: &crate::Mf4File) -> Result<Self> {
let (writer, report) = Self::from_file_with_report(file, RewriteMode::BestEffort)?;
for issue in report.issues {
log::warn!(
"rewrite {:?} '{}': {}",
issue.location,
issue.name,
issue.reason
);
}
Ok(writer)
}
pub fn from_file_with_report(
file: &crate::Mf4File,
mode: RewriteMode,
) -> Result<(Self, RewriteReport)> {
let mut writer = Mf4Writer::with_start_time_ns(file.start_time().timestamp_ns);
let mut report = RewriteReport::default();
if !file.comment().is_empty() {
report.note(
None,
"file comment",
"not represented by the editable writer",
);
}
for entry in file.file_history() {
report.note(
None,
"file history",
format!(
"history entry replaced by new writer history: {}",
entry.comment
),
);
}
for entry in file.attachments() {
report.note(None, &entry.file_name, "attachment omitted");
}
for entry in file.events() {
report.note(None, &entry.name, "event omitted");
}
for entry in file.channel_hierarchy() {
report.note(
None,
&entry.name,
"channel hierarchy and descendants omitted",
);
}
for (dg_idx, dg) in file.data_groups().iter().enumerate() {
if !dg.comment.is_empty() {
report.note(
None,
&format!("data group {dg_idx}"),
"data-group comment omitted",
);
}
let mut first_group_idx = None;
for (cg_idx, cg) in dg.channel_groups.iter().enumerate() {
if !cg.acquisition_name.is_empty() || !cg.comment.is_empty() || cg.source.is_some()
{
report.note(
None,
&format!("group {dg_idx}.{cg_idx}"),
"acquisition name, comment or source metadata omitted",
);
}
let master = cg.master_channel();
let times = if let Some(master) = master {
match file.channel_timestamps(master) {
Ok(times) => times,
Err(error) => {
for ch in &cg.channels {
report.note(
Some((dg_idx, cg_idx, ch.index)),
&ch.name,
format!("group omitted: {error}"),
);
}
continue;
}
}
} else {
report.note(
None,
&format!("group {dg_idx}.{cg_idx}"),
"masterless group receives a sample-index master",
);
(0..cg.sample_count).map(|i| i as f64).collect()
};
if let Some(master) = master {
report.note(Some((dg_idx, cg_idx, master.index)), &master.name,
"master is rewritten as physical f64 Time [s]; raw representation, conversion, synchronization and metadata are not preserved");
}
let index = writer.groups.len();
let group = match first_group_idx {
Some(sibling) => writer.add_group_in(sibling, ×)?,
None => {
first_group_idx = Some(index);
writer.add_group(×)?
}
};
for ch in &cg.channels {
if ch.is_master() {
continue;
}
let location = Some((dg_idx, cg_idx, ch.index));
let imported = (|| -> Result<()> {
if let Some(reason) = ch.unreadable() {
return Err(Mf4Error::write_error(reason.to_string()));
}
let sig = file.signal(ch)?;
let raw_vals = sig.raw_values()?;
let is_vlsd = ch.channel_type == ChannelType::VariableLength;
let conv = (!ch.conversion.is_identity()).then(|| ch.conversion.clone());
group.add_channel_internal(
&ch.name,
&ch.unit,
&ch.comment,
raw_vals,
sig.validity().as_deref(),
conv,
is_vlsd,
ch.array_shape.clone(),
)?;
Ok(())
})();
match imported {
Err(error) => {
report.note(location, &ch.name, format!("channel omitted: {error}"))
}
Ok(()) => {
if ch.source.is_some()
|| ch.min_value.is_some()
|| ch.max_value.is_some()
{
report.note(
location,
&ch.name,
"source or declared range metadata omitted",
);
}
}
}
}
}
}
if mode == RewriteMode::Strict && !report.is_lossless() {
return Err(Mf4Error::write_error(format!(
"strict rewrite rejected {} issues: {}",
report.issues.len(),
report
.issues
.iter()
.map(|i| format!("{:?} '{}': {}", i.location, i.name, i.reason))
.collect::<Vec<_>>()
.join("; ")
)));
}
Ok((writer, report))
}
pub fn write<W: Write>(&self, out: &mut W) -> Result<()> {
let fh_text = format!("created by falcon_mdf {}", env!("CARGO_PKG_VERSION"));
let mut dg_groups: Vec<(usize, Vec<usize>)> = Vec::new();
for (group_idx, group) in self.groups.iter().enumerate() {
if let Some((_, list)) = dg_groups.iter_mut().find(|(id, _)| *id == group.dg_id) {
list.push(group_idx);
} else {
dg_groups.push((group.dg_id, vec![group_idx]));
}
}
let payloads: Vec<Payload> = dg_groups
.iter()
.map(|(_, group_indices)| {
let groups: Vec<&WriteGroup> =
group_indices.iter().map(|&idx| &self.groups[idx]).collect();
Payload::build(&groups, self.compress, self.codec)
})
.collect::<Result<_>>()?;
let mut next = ID_SIZE + HD_SIZE;
let fh_off = next;
next += FH_SIZE;
let fh_tx_off = next;
next += tx_size(&fh_text);
let layouts: Vec<DataGroupLayout> = dg_groups
.iter()
.zip(&payloads)
.map(|((_dg_id, group_indices), payload)| {
let dg_off = next;
next += DG_SIZE;
let group_count = group_indices.len();
let rec_id_size: u8 = if group_count > 1 { 1 } else { 0 };
let mut cgs = Vec::with_capacity(group_count);
for (cg_idx, &group_idx) in group_indices.iter().enumerate() {
let group = &self.groups[group_idx];
let record_id = if group_count > 1 {
(cg_idx + 1) as u64
} else {
0
};
let cg_off = next;
next += CG_SIZE;
let inval_bits = inval_bit_indices(group);
let offsets = byte_offsets(group);
let mut channels = Vec::with_capacity(group.channels.len() + 1);
channels.push(ChannelLayout::master(next));
next += channels[0].size;
for (index, channel) in group.channels.iter().enumerate() {
let (flags, bit) = match inval_bits[index] {
Some(bit) => (CN_FLAG_INVALIDATION_BIT, bit),
None => (0, 0),
};
let layout = ChannelLayout::new(next, offsets[index], channel, flags, bit)?;
next += layout.size;
channels.push(layout);
}
cgs.push(CgLayout {
cg_off,
record_id,
group_idx,
channels,
});
}
let dt_off = next;
next += payload.size();
Ok(DataGroupLayout {
dg_off,
rec_id_size,
cgs,
dt_off,
})
})
.collect::<Result<Vec<_>>>()?;
write_id(out)?;
write_hd(
out,
layouts.first().map(|l| l.dg_off).unwrap_or(0),
fh_off,
self.start_time_ns,
)?;
write_fh(out, fh_tx_off, self.start_time_ns)?;
write_tx(out, &fh_text)?;
for (dg_idx, (layout, payload)) in layouts.iter().zip(&payloads).enumerate() {
let dg_next = layouts.get(dg_idx + 1).map(|l| l.dg_off).unwrap_or(0);
let cg_first = layout.cgs.first().map(|cg| cg.cg_off).unwrap_or(0);
write_dg(out, dg_next, cg_first, layout.dt_off, layout.rec_id_size)?;
for (cg_idx, cg_layout) in layout.cgs.iter().enumerate() {
let group = &self.groups[cg_layout.group_idx];
let cg_next = layout.cgs.get(cg_idx + 1).map(|c| c.cg_off).unwrap_or(0);
write_cg(
out,
group,
cg_layout.channels[0].cn_off,
cg_next,
cg_layout.record_id,
)?;
for (channel_index, channel_layout) in cg_layout.channels.iter().enumerate() {
let cn_next = cg_layout
.channels
.get(channel_index + 1)
.map(|c| c.cn_off)
.unwrap_or(0);
write_cn(out, channel_layout, cn_next)?;
write_tx(out, &channel_layout.name)?;
if !channel_layout.unit.is_empty() {
write_tx(out, &channel_layout.unit)?;
}
if !channel_layout.comment.is_empty() {
write_tx(out, &channel_layout.comment)?;
}
if let Some((cc_off, plan)) = &channel_layout.cc {
write_cc(out, *cc_off, plan)?;
for text in plan.refs.iter().flatten() {
write_tx(out, text)?;
}
}
if let Some((_, shape, stride)) = &channel_layout.ca {
write_ca(out, shape, *stride)?;
}
if let Some((_, sd_data)) = &channel_layout.sd {
write_sd(out, sd_data)?;
}
}
}
write_payload(out, layout.dt_off, payload)?;
}
Ok(())
}
pub fn write_to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
let file = std::fs::File::create(path)?;
let mut out = std::io::BufWriter::new(file);
self.write(&mut out)?;
out.flush()?;
Ok(())
}
}
impl WriteGroup {
pub fn times(&self) -> &[f64] {
&self.times
}
pub fn set_times(&mut self, times: &[f64]) -> Result<()> {
if times.iter().any(|t| t.is_nan()) {
return Err(Mf4Error::write_error(
"time axis contains NaN, which cannot be ordered",
));
}
for ch in &self.channels {
if ch.values.len() != times.len() {
return Err(Mf4Error::write_error(format!(
"channel '{}' has {} values, but new time axis has {}",
ch.name,
ch.values.len(),
times.len()
)));
}
}
self.times = times.to_vec();
Ok(())
}
pub fn channels(&self) -> &[WriteChannel] {
&self.channels
}
pub fn channels_mut(&mut self) -> &mut [WriteChannel] {
&mut self.channels
}
pub fn find_channel(&self, name: &str) -> Option<&WriteChannel> {
self.channels.iter().find(|c| c.name == name)
}
pub fn find_channel_mut(&mut self, name: &str) -> Option<&mut WriteChannel> {
self.channels.iter_mut().find(|c| c.name == name)
}
pub fn remove_channel(&mut self, index: usize) -> WriteChannel {
self.channels.remove(index)
}
pub fn remove_channel_by_name(&mut self, name: &str) -> Option<WriteChannel> {
let idx = self.channels.iter().position(|c| c.name == name)?;
Some(self.channels.remove(idx))
}
pub fn retain_channels<F: FnMut(&WriteChannel) -> bool>(&mut self, mut f: F) {
self.channels.retain(|c| f(c));
}
pub fn add_channel(&mut self, name: &str, unit: &str, values: &[f64]) -> Result<()> {
self.add_channel_with_validity(name, unit, values, None)
}
pub fn add_channel_with_validity(
&mut self,
name: &str,
unit: &str,
values: &[f64],
valid: Option<&[bool]>,
) -> Result<()> {
self.add_channel_typed_with(name, unit, SignalValues::F64(values.to_vec()), valid, None)
}
pub fn add_channel_typed(
&mut self,
name: &str,
unit: &str,
values: SignalValues,
) -> Result<()> {
self.add_channel_typed_with(name, unit, values, None, None)
}
pub fn add_channel_typed_with(
&mut self,
name: &str,
unit: &str,
values: SignalValues,
valid: Option<&[bool]>,
conversion: Option<Conversion>,
) -> Result<()> {
self.add_channel_full(name, unit, "", values, valid, conversion)
}
pub fn add_channel_full(
&mut self,
name: &str,
unit: &str,
comment: &str,
values: SignalValues,
valid: Option<&[bool]>,
conversion: Option<Conversion>,
) -> Result<()> {
self.add_channel_internal(name, unit, comment, values, valid, conversion, false, None)
}
pub fn add_channel_vlsd_str(
&mut self,
name: &str,
unit: &str,
strings: &[impl AsRef<str>],
) -> Result<()> {
let vals: Vec<String> = strings.iter().map(|s| s.as_ref().to_string()).collect();
self.add_channel_vlsd(name, unit, SignalValues::Str(vals))
}
pub fn add_channel_vlsd_bytes(
&mut self,
name: &str,
unit: &str,
byte_slices: &[&[u8]],
) -> Result<()> {
let mut data = Vec::new();
let mut starts = Vec::with_capacity(byte_slices.len() + 1);
starts.push(0);
for s in byte_slices {
data.extend_from_slice(s);
starts.push(data.len());
}
self.add_channel_vlsd(name, unit, SignalValues::VarBytes { data, starts })
}
pub fn add_channel_vlsd(&mut self, name: &str, unit: &str, values: SignalValues) -> Result<()> {
self.add_channel_vlsd_with(name, unit, values, None, None)
}
pub fn add_channel_vlsd_with(
&mut self,
name: &str,
unit: &str,
values: SignalValues,
valid: Option<&[bool]>,
conversion: Option<Conversion>,
) -> Result<()> {
self.add_channel_vlsd_full(name, unit, "", values, valid, conversion)
}
pub fn add_channel_vlsd_full(
&mut self,
name: &str,
unit: &str,
comment: &str,
values: SignalValues,
valid: Option<&[bool]>,
conversion: Option<Conversion>,
) -> Result<()> {
self.add_channel_internal(name, unit, comment, values, valid, conversion, true, None)
}
pub fn add_channel_array(
&mut self,
name: &str,
unit: &str,
shape: &[u64],
values: SignalValues,
) -> Result<()> {
self.add_channel_array_with(name, unit, shape, values, None, None)
}
pub fn add_channel_array_with(
&mut self,
name: &str,
unit: &str,
shape: &[u64],
values: SignalValues,
valid: Option<&[bool]>,
conversion: Option<Conversion>,
) -> Result<()> {
self.add_channel_array_full(name, unit, "", shape, values, valid, conversion)
}
#[allow(clippy::too_many_arguments)]
pub fn add_channel_array_full(
&mut self,
name: &str,
unit: &str,
comment: &str,
shape: &[u64],
values: SignalValues,
valid: Option<&[bool]>,
conversion: Option<Conversion>,
) -> Result<()> {
if shape.is_empty() || shape.contains(&0) {
return Err(Mf4Error::write_error(format!(
"array channel '{name}' has invalid shape {shape:?}; dimensions must be non-empty and non-zero"
)));
}
let total: u64 = shape
.iter()
.copied()
.try_fold(1u64, |acc, d| acc.checked_mul(d))
.ok_or_else(|| {
Mf4Error::write_error(format!(
"array channel '{name}' shape {shape:?} overflows element count"
))
})?;
if let SignalValues::Array {
elements_per_sample,
..
} = &values
{
if *elements_per_sample as u64 != total {
return Err(Mf4Error::write_error(format!(
"array channel '{name}' declared shape {shape:?} implies {total} elements per sample, but values has {elements_per_sample}"
)));
}
} else {
return Err(Mf4Error::write_error(format!(
"channel '{name}' holds {} samples, which cannot be written as an array channel; use SignalValues::Array",
values.kind()
)));
}
self.add_channel_internal(
name,
unit,
comment,
values,
valid,
conversion,
false,
Some(shape.to_vec()),
)
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn add_channel_internal(
&mut self,
name: &str,
unit: &str,
comment: &str,
values: SignalValues,
valid: Option<&[bool]>,
conversion: Option<Conversion>,
is_vlsd: bool,
array_shape: Option<Vec<u64>>,
) -> Result<()> {
if values.len() != self.times.len() {
return Err(Mf4Error::write_error(format!(
"channel '{name}' has {} values but the group's time axis has {}",
values.len(),
self.times.len()
)));
}
let valid = match valid {
None => None,
Some(valid) => {
if valid.len() != self.times.len() {
return Err(Mf4Error::write_error(format!(
"channel '{name}' has {} validity flags but the group's time axis has {}",
valid.len(),
self.times.len()
)));
}
Some(valid.to_vec())
}
};
let format = SampleFormat::of(&values, name, is_vlsd)?;
let is_vlsd = format.is_vlsd;
let array_shape = match (array_shape, &values) {
(Some(shape), _) => Some(shape),
(
None,
SignalValues::Array {
elements_per_sample,
..
},
) => Some(vec![*elements_per_sample as u64]),
(None, _) => None,
};
let conversion = match conversion {
Some(c) if !matches!(c, Conversion::None) => {
CcPlan::of(&c, name)?;
Some(c)
}
_ => None,
};
self.channels.push(WriteChannel {
name: name.to_string(),
unit: unit.to_string(),
comment: comment.to_string(),
values,
valid,
conversion,
format,
is_vlsd,
array_shape,
});
Ok(())
}
}
#[derive(Debug)]
struct CcPlan {
cc_type: u8,
values: Vec<f64>,
refs: Vec<Option<String>>,
}
impl CcPlan {
fn of(conversion: &Conversion, name: &str) -> Result<Option<Self>> {
let refused = |what: &str| {
Err(Mf4Error::write_error(format!(
"channel '{name}' carries {what}, which this writer cannot express; \
writing the channel without it would return raw counts labelled as \
physical values"
)))
};
Ok(match conversion {
Conversion::None => None,
Conversion::Linear { offset, factor } if *offset == 0.0 && *factor == 1.0 => None,
Conversion::Linear { offset, factor } => Some(CcPlan {
cc_type: 1,
values: vec![*offset, *factor],
refs: Vec::new(),
}),
Conversion::Rational { coefficients } => Some(CcPlan {
cc_type: 2,
values: coefficients.to_vec(),
refs: Vec::new(),
}),
Conversion::Algebraic { formula, .. } => Some(CcPlan {
cc_type: 3,
values: Vec::new(),
refs: vec![Some(formula.clone())],
}),
Conversion::TableInterpolated { keys, values }
| Conversion::TableLookup { keys, values } => {
if keys.is_empty() || keys.len() != values.len() {
return refused(
"a conversion table with no entries, or with more keys than values",
);
}
let cc_type = if matches!(conversion, Conversion::TableInterpolated { .. }) {
4
} else {
5
};
let mut interleaved = Vec::with_capacity(keys.len() * 2);
for (k, v) in keys.iter().zip(values) {
interleaved.push(*k);
interleaved.push(*v);
}
Some(CcPlan {
cc_type,
values: interleaved,
refs: Vec::new(),
})
}
Conversion::ValueToText {
keys,
entries,
default,
} => {
if keys.len() != entries.len() {
return refused("a value-to-text table with more keys than entries");
}
let mut refs = Vec::with_capacity(entries.len() + 1);
for entry in entries {
match entry {
TableEntry::Text(t) => refs.push(Some(t.clone())),
TableEntry::Nested(_) => {
return refused("a value-to-text table with a nested conversion")
}
}
}
refs.push(match default {
Some(TableEntry::Text(t)) => Some(t.clone()),
Some(TableEntry::Nested(_)) => {
return refused("a value-to-text table with a nested default")
}
None => None,
});
Some(CcPlan {
cc_type: 7,
values: keys.clone(),
refs,
})
}
Conversion::Unsupported { kind, .. } => {
return refused(&format!(
"a conversion of type {kind:?}, which the reader \
itself could not evaluate"
))
}
other => return refused(&format!("a conversion of kind {other:?}")),
})
}
fn size(&self) -> u64 {
CC_HEADER_SIZE + (4 + self.refs.len() as u64) * 8 + 24 + self.values.len() as u64 * 8
}
}
#[derive(Debug)]
struct ChannelLayout {
cn_off: u64,
size: u64,
name: String,
unit: String,
comment: String,
byte_offset: u32,
flags: u32,
inval_bit_pos: u32,
data_type: u8,
bit_count: u32,
channel_type: u8,
cc: Option<(u64, CcPlan)>,
ca: Option<(u64, Vec<u64>, i32)>,
sd: Option<(u64, Vec<u8>)>,
}
impl ChannelLayout {
fn master(offset: u64) -> Self {
ChannelLayout {
cn_off: offset,
size: CN_SIZE + tx_size("Time") + tx_size("s"),
name: "Time".to_string(),
unit: "s".to_string(),
comment: String::new(),
byte_offset: 0,
flags: 0,
inval_bit_pos: 0,
data_type: 4,
bit_count: 64,
channel_type: 2,
cc: None,
ca: None,
sd: None,
}
}
fn new(
offset: u64,
byte_offset: u32,
channel: &WriteChannel,
flags: u32,
inval_bit_pos: u32,
) -> Result<Self> {
let mut size = CN_SIZE + tx_size(&channel.name);
if !channel.unit.is_empty() {
size += tx_size(&channel.unit);
}
if !channel.comment.is_empty() {
size += tx_size(&channel.comment);
}
let cc = match &channel.conversion {
None => None,
Some(c) => CcPlan::of(c, &channel.name)?.map(|plan| {
let cc_off = offset + size;
size += plan.size();
for text in plan.refs.iter().flatten() {
size += tx_size(text);
}
(cc_off, plan)
}),
};
let ca = if let Some(shape) = &channel.array_shape {
let ca_off = offset + size;
let ca_sz = 48 + (shape.len() as u64) * 8;
size += ca_sz;
Some((ca_off, shape.clone(), channel.format.width as i32))
} else {
None
};
let (sd, channel_type, flags) = if channel.is_vlsd {
let (sd_data, _) = build_sd_data(&channel.values);
let sd_off = offset + size;
size += SD_HEADER_SIZE + sd_data.len() as u64;
(Some((sd_off, sd_data)), 1, flags | CN_FLAG_VLSD_OFFSET)
} else {
(None, 0, flags)
};
Ok(ChannelLayout {
cn_off: offset,
size,
name: channel.name.clone(),
unit: channel.unit.clone(),
comment: channel.comment.clone(),
byte_offset,
flags,
inval_bit_pos,
data_type: channel.format.data_type,
bit_count: channel.format.width * 8,
channel_type,
cc,
ca,
sd,
})
}
fn unit_off(&self) -> u64 {
if self.unit.is_empty() {
0
} else {
self.cn_off + CN_SIZE + tx_size(&self.name)
}
}
fn comment_off(&self) -> u64 {
if self.comment.is_empty() {
0
} else {
let mut off = self.cn_off + CN_SIZE + tx_size(&self.name);
if !self.unit.is_empty() {
off += tx_size(&self.unit);
}
off
}
}
fn cc_off(&self) -> u64 {
self.cc.as_ref().map(|(off, _)| *off).unwrap_or(0)
}
fn ca_off(&self) -> u64 {
self.ca.as_ref().map(|(off, _, _)| *off).unwrap_or(0)
}
fn sd_off(&self) -> u64 {
self.sd.as_ref().map(|(off, _)| *off).unwrap_or(0)
}
}
#[derive(Debug)]
enum Payload {
Plain(Vec<u8>),
Compressed {
zip_type: u8,
zip_parameter: u32,
original_len: u64,
data: Vec<u8>,
},
}
impl Payload {
fn build(groups: &[&WriteGroup], compress: bool, codec: WriteCodec) -> Result<Self> {
let raw = record_bytes_multi(groups);
if !compress {
return Ok(Payload::Plain(raw));
}
if groups.len() > 1 && codec.is_transposed() {
return Err(Mf4Error::write_error(format!(
"transposed compression codec {codec:?} requires a uniform record size, but this data group interleaves {} channel groups",
groups.len()
)));
}
let col_size = if groups.len() == 1 {
record_size(groups[0]) as usize
} else {
0
};
let (transposed, zip_parameter) = if codec.is_transposed() {
(Some(transpose(&raw, col_size)), col_size as u32)
} else {
(None, 0u32)
};
let slice_to_compress = transposed.as_deref().unwrap_or(&raw);
let compressed_bytes = match codec {
WriteCodec::Deflate | WriteCodec::TransposedDeflate => {
use flate2::write::ZlibEncoder;
use flate2::Compression;
let mut encoder = ZlibEncoder::new(
Vec::with_capacity(slice_to_compress.len() / 2),
Compression::default(),
);
encoder.write_all(slice_to_compress)?;
encoder
.finish()
.map_err(|e| Mf4Error::Compression(e.to_string()))?
}
#[cfg(feature = "lz4")]
WriteCodec::Lz4 | WriteCodec::TransposedLz4 => {
use lz4_flex::frame::FrameEncoder;
let mut encoder = FrameEncoder::new(Vec::new());
encoder.write_all(slice_to_compress)?;
encoder
.finish()
.map_err(|e| Mf4Error::Compression(e.to_string()))?
}
};
drop(transposed);
Ok(Payload::Compressed {
zip_type: codec.zip_type(),
zip_parameter,
original_len: raw.len() as u64,
data: compressed_bytes,
})
}
fn size(&self) -> u64 {
match self {
Payload::Plain(data) => DT_HEADER_SIZE + data.len() as u64,
Payload::Compressed { data, .. } => {
HL_SIZE + DL_SIZE + DZ_HEADER_SIZE + data.len() as u64
}
}
}
}
#[derive(Debug)]
struct CgLayout {
cg_off: u64,
record_id: u64,
group_idx: usize,
channels: Vec<ChannelLayout>,
}
#[derive(Debug)]
struct DataGroupLayout {
dg_off: u64,
rec_id_size: u8,
cgs: Vec<CgLayout>,
dt_off: u64,
}
fn sorted_order(times: &[f64]) -> Vec<usize> {
let mut order: Vec<usize> = (0..times.len()).collect();
order.sort_by(|&a, &b| times[a].total_cmp(×[b]));
order
}
fn data_bytes(group: &WriteGroup) -> u64 {
SAMPLE_BYTES
+ group
.channels
.iter()
.map(|c| u64::from(c.format.record_width))
.sum::<u64>()
}
fn record_size(group: &WriteGroup) -> u64 {
data_bytes(group) + u64::from(inval_bytes(group))
}
fn byte_offsets(group: &WriteGroup) -> Vec<u32> {
let mut next = SAMPLE_BYTES as u32;
group
.channels
.iter()
.map(|c| {
let at = next;
next += c.format.record_width;
at
})
.collect()
}
fn inval_bytes(group: &WriteGroup) -> u32 {
let bits = group.channels.iter().filter(|c| c.valid.is_some()).count();
bits.div_ceil(8) as u32
}
fn inval_bit_indices(group: &WriteGroup) -> Vec<Option<u32>> {
let mut next_bit = 0u32;
group
.channels
.iter()
.map(|c| {
if c.valid.is_some() {
let bit = next_bit;
next_bit += 1;
Some(bit)
} else {
None
}
})
.collect()
}
fn tx_size(text: &str) -> u64 {
24 + text.len() as u64 + 1
}
fn block_header(buf: &mut Vec<u8>, id: &[u8; 4], length: u64, link_count: u64) {
buf.extend_from_slice(id);
buf.extend_from_slice(&[0u8; 4]);
buf.extend_from_slice(&length.to_le_bytes());
buf.extend_from_slice(&link_count.to_le_bytes());
}
fn push_link(buf: &mut Vec<u8>, target: u64) {
buf.extend_from_slice(&target.to_le_bytes());
}
fn write_id(out: &mut impl Write) -> Result<()> {
let mut buf = vec![0u8; ID_SIZE as usize];
buf[0..8].copy_from_slice(b"MDF ");
buf[8..16].copy_from_slice(b"4.11 ");
buf[16..24].copy_from_slice(b"falcon ");
buf[28..30].copy_from_slice(&411u16.to_le_bytes());
out.write_all(&buf)?;
Ok(())
}
fn write_hd(out: &mut impl Write, dg_first: u64, fh_first: u64, start_time_ns: i64) -> Result<()> {
let mut buf = Vec::with_capacity(HD_SIZE as usize);
block_header(&mut buf, b"##HD", HD_SIZE, 6);
push_link(&mut buf, dg_first);
push_link(&mut buf, fh_first);
push_link(&mut buf, 0); push_link(&mut buf, 0); push_link(&mut buf, 0); push_link(&mut buf, 0); buf.extend_from_slice(&start_time_ns.to_le_bytes());
buf.extend_from_slice(&0i16.to_le_bytes()); buf.extend_from_slice(&0i16.to_le_bytes()); buf.push(0); buf.push(0); buf.push(0); buf.push(0); buf.extend_from_slice(&0f64.to_le_bytes()); buf.extend_from_slice(&0f64.to_le_bytes()); out.write_all(&buf)?;
Ok(())
}
fn write_fh(out: &mut impl Write, md_comment: u64, time_ns: i64) -> Result<()> {
let mut buf = Vec::with_capacity(FH_SIZE as usize);
block_header(&mut buf, b"##FH", FH_SIZE, 2);
push_link(&mut buf, 0); push_link(&mut buf, md_comment);
buf.extend_from_slice(&(time_ns.max(0) as u64).to_le_bytes());
buf.extend_from_slice(&0i16.to_le_bytes());
buf.extend_from_slice(&0i16.to_le_bytes());
buf.push(0); buf.extend_from_slice(&[0u8; 3]); out.write_all(&buf)?;
Ok(())
}
fn write_tx(out: &mut impl Write, text: &str) -> Result<()> {
let length = tx_size(text);
let mut buf = Vec::with_capacity(length as usize);
block_header(&mut buf, b"##TX", length, 0);
buf.extend_from_slice(text.as_bytes());
buf.push(0);
out.write_all(&buf)?;
Ok(())
}
fn write_dg(
out: &mut impl Write,
dg_next: u64,
cg_first: u64,
data: u64,
rec_id_size: u8,
) -> Result<()> {
let mut buf = Vec::with_capacity(DG_SIZE as usize);
block_header(&mut buf, b"##DG", DG_SIZE, 4);
push_link(&mut buf, dg_next);
push_link(&mut buf, cg_first);
push_link(&mut buf, data);
push_link(&mut buf, 0); buf.push(rec_id_size); buf.extend_from_slice(&[0u8; 7]); out.write_all(&buf)?;
Ok(())
}
fn write_cg(
out: &mut impl Write,
group: &WriteGroup,
cn_first: u64,
cg_next: u64,
record_id: u64,
) -> Result<()> {
let mut buf = Vec::with_capacity(CG_SIZE as usize);
block_header(&mut buf, b"##CG", CG_SIZE, 6);
push_link(&mut buf, cg_next);
push_link(&mut buf, cn_first);
push_link(&mut buf, 0); push_link(&mut buf, 0); push_link(&mut buf, 0); push_link(&mut buf, 0); buf.extend_from_slice(&record_id.to_le_bytes()); buf.extend_from_slice(&(group.times.len() as u64).to_le_bytes());
buf.extend_from_slice(&0u16.to_le_bytes()); buf.extend_from_slice(&0u16.to_le_bytes()); buf.extend_from_slice(&[0u8; 4]); buf.extend_from_slice(&(data_bytes(group) as u32).to_le_bytes());
buf.extend_from_slice(&inval_bytes(group).to_le_bytes());
out.write_all(&buf)?;
Ok(())
}
fn write_cn(out: &mut impl Write, layout: &ChannelLayout, cn_next: u64) -> Result<()> {
let mut buf = Vec::with_capacity(CN_SIZE as usize);
block_header(&mut buf, b"##CN", CN_SIZE, 8);
push_link(&mut buf, cn_next);
push_link(&mut buf, layout.ca_off()); push_link(&mut buf, layout.cn_off + CN_SIZE); push_link(&mut buf, 0); push_link(&mut buf, layout.cc_off()); push_link(&mut buf, layout.sd_off()); push_link(&mut buf, layout.unit_off());
push_link(&mut buf, layout.comment_off());
buf.push(layout.channel_type);
buf.push(if layout.channel_type == 2 { 1 } else { 0 }); buf.push(layout.data_type);
buf.push(0); buf.extend_from_slice(&layout.byte_offset.to_le_bytes());
buf.extend_from_slice(&layout.bit_count.to_le_bytes());
buf.extend_from_slice(&layout.flags.to_le_bytes());
buf.extend_from_slice(&layout.inval_bit_pos.to_le_bytes());
buf.push(0); buf.push(0); buf.extend_from_slice(&0u16.to_le_bytes()); for _ in 0..6 {
buf.extend_from_slice(&0f64.to_le_bytes());
}
out.write_all(&buf)?;
Ok(())
}
fn write_ca(out: &mut impl Write, shape: &[u64], stride: i32) -> Result<()> {
let ndim = shape.len() as u16;
let length = 48 + (ndim as u64) * 8;
let mut buf = Vec::with_capacity(length as usize);
block_header(&mut buf, b"##CA", length, 1);
push_link(&mut buf, 0); buf.push(0); buf.push(0); buf.extend_from_slice(&ndim.to_le_bytes());
buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&stride.to_le_bytes()); buf.extend_from_slice(&0u32.to_le_bytes()); for &d in shape {
buf.extend_from_slice(&d.to_le_bytes());
}
out.write_all(&buf)?;
Ok(())
}
fn write_sd(out: &mut impl Write, data: &[u8]) -> Result<()> {
let length = SD_HEADER_SIZE + data.len() as u64;
let mut buf = Vec::with_capacity(SD_HEADER_SIZE as usize);
block_header(&mut buf, b"##SD", length, 0);
out.write_all(&buf)?;
out.write_all(data)?;
Ok(())
}
fn write_cc(out: &mut impl Write, cc_off: u64, plan: &CcPlan) -> Result<()> {
let length = plan.size();
let mut buf = Vec::with_capacity(length as usize);
block_header(&mut buf, b"##CC", length, 4 + plan.refs.len() as u64);
push_link(&mut buf, 0); push_link(&mut buf, 0); push_link(&mut buf, 0); push_link(&mut buf, 0);
let mut text_off = cc_off
+ CC_HEADER_SIZE
+ (4 + plan.refs.len() as u64) * 8
+ 24
+ plan.values.len() as u64 * 8;
for text in &plan.refs {
match text {
Some(t) => {
push_link(&mut buf, text_off);
text_off += tx_size(t);
}
None => push_link(&mut buf, 0),
}
}
buf.push(plan.cc_type);
buf.push(0); buf.extend_from_slice(&0u16.to_le_bytes()); buf.extend_from_slice(&(plan.refs.len() as u16).to_le_bytes());
buf.extend_from_slice(&(plan.values.len() as u16).to_le_bytes());
buf.extend_from_slice(&0f64.to_le_bytes()); buf.extend_from_slice(&0f64.to_le_bytes()); for v in &plan.values {
buf.extend_from_slice(&v.to_le_bytes());
}
debug_assert_eq!(
buf.len() as u64,
length,
"CC block size must match its header"
);
out.write_all(&buf)?;
Ok(())
}
fn record_bytes(group: &WriteGroup) -> Vec<u8> {
let size = record_size(group);
let mut buf = Vec::with_capacity(group.times.len() * size as usize);
let order;
let indices: &mut dyn Iterator<Item = usize> =
if group.times.is_sorted_by(|a, b| a.total_cmp(b).is_le()) {
&mut (0..group.times.len())
} else {
order = sorted_order(&group.times);
&mut order.into_iter()
};
let inval_bits = inval_bit_indices(group);
let inval_len = inval_bytes(group) as usize;
let mut inval = vec![0u8; inval_len];
let vlsd_offsets: Vec<Option<Vec<u64>>> = group
.channels
.iter()
.map(|c| {
if c.is_vlsd {
let (_, offsets) = build_sd_data(&c.values);
Some(offsets)
} else {
None
}
})
.collect();
for index in indices {
buf.extend_from_slice(&group.times[index].to_le_bytes());
for (channel_idx, channel) in group.channels.iter().enumerate() {
if let Some(offsets) = &vlsd_offsets[channel_idx] {
buf.extend_from_slice(&offsets[index].to_le_bytes());
} else {
channel.format.encode(&channel.values, index, &mut buf);
}
}
if inval_len > 0 {
inval.fill(0);
for (channel, bit) in group.channels.iter().zip(&inval_bits) {
let Some(bit) = bit else { continue };
let valid = channel.valid.as_ref().expect("a bit implies validity");
if !valid[index] {
inval[(bit / 8) as usize] |= 1 << (bit % 8);
}
}
buf.extend_from_slice(&inval);
}
}
buf
}
fn record_bytes_multi(groups: &[&WriteGroup]) -> Vec<u8> {
if groups.is_empty() {
return Vec::new();
}
if groups.len() == 1 {
return record_bytes(groups[0]);
}
struct SampleRef {
time: f64,
group_idx: usize,
sample_idx: usize,
}
let mut samples = Vec::new();
let mut total_capacity: usize = 0;
for (group_idx, group) in groups.iter().enumerate() {
let rec_len = 1 + record_size(group) as usize;
total_capacity += group.times.len() * rec_len;
for (sample_idx, &time) in group.times.iter().enumerate() {
samples.push(SampleRef {
time,
group_idx,
sample_idx,
});
}
}
samples.sort_by(|a, b| {
a.time
.total_cmp(&b.time)
.then_with(|| a.group_idx.cmp(&b.group_idx))
.then_with(|| a.sample_idx.cmp(&b.sample_idx))
});
let inval_bits: Vec<Vec<Option<u32>>> = groups.iter().map(|g| inval_bit_indices(g)).collect();
let inval_lens: Vec<usize> = groups.iter().map(|g| inval_bytes(g) as usize).collect();
let vlsd_offsets: Vec<Vec<Option<Vec<u64>>>> = groups
.iter()
.map(|g| {
g.channels
.iter()
.map(|c| {
if c.is_vlsd {
let (_, offsets) = build_sd_data(&c.values);
Some(offsets)
} else {
None
}
})
.collect()
})
.collect();
let mut buf = Vec::with_capacity(total_capacity);
let mut inval_scratch: Vec<Vec<u8>> = inval_lens.iter().map(|&len| vec![0u8; len]).collect();
for sample in samples {
let group = groups[sample.group_idx];
let record_id = (sample.group_idx + 1) as u8;
buf.push(record_id);
buf.extend_from_slice(&sample.time.to_le_bytes());
for (channel_idx, channel) in group.channels.iter().enumerate() {
if let Some(offsets) = &vlsd_offsets[sample.group_idx][channel_idx] {
buf.extend_from_slice(&offsets[sample.sample_idx].to_le_bytes());
} else {
channel
.format
.encode(&channel.values, sample.sample_idx, &mut buf);
}
}
let inval_len = inval_lens[sample.group_idx];
if inval_len > 0 {
let inval = &mut inval_scratch[sample.group_idx];
inval.fill(0);
for (channel, bit) in group.channels.iter().zip(&inval_bits[sample.group_idx]) {
let Some(bit) = bit else { continue };
let valid = channel.valid.as_ref().expect("a bit implies validity");
if !valid[sample.sample_idx] {
inval[(bit / 8) as usize] |= 1 << (bit % 8);
}
}
buf.extend_from_slice(inval);
}
}
buf
}
pub fn transpose(raw: &[u8], column_size: usize) -> Vec<u8> {
if column_size == 0 {
return raw.to_vec();
}
let lines = raw.len() / column_size;
if lines == 0 {
return raw.to_vec();
}
let prefix_len = lines * column_size;
let mut result = vec![0u8; raw.len()];
for line in 0..lines {
let mut dst = line;
for &byte in &raw[line * column_size..][..column_size] {
result[dst] = byte;
dst += lines;
}
}
result[prefix_len..].copy_from_slice(&raw[prefix_len..]);
result
}
fn write_payload(out: &mut impl Write, at: u64, payload: &Payload) -> Result<()> {
match payload {
Payload::Plain(data) => {
let mut buf = Vec::with_capacity(DT_HEADER_SIZE as usize);
block_header(&mut buf, b"##DT", DT_HEADER_SIZE + data.len() as u64, 0);
out.write_all(&buf)?;
out.write_all(data)?;
}
Payload::Compressed {
zip_type,
zip_parameter,
original_len,
data,
} => {
let dl_off = at + HL_SIZE;
let dz_off = dl_off + DL_SIZE;
let mut buf = Vec::with_capacity(HL_SIZE as usize);
block_header(&mut buf, b"##HL", HL_SIZE, 1);
push_link(&mut buf, dl_off);
buf.extend_from_slice(&0u16.to_le_bytes()); buf.push(*zip_type); buf.extend_from_slice(&[0u8; 5]); out.write_all(&buf)?;
let mut buf = Vec::with_capacity(DL_SIZE as usize);
block_header(&mut buf, b"##DL", DL_SIZE, 2);
push_link(&mut buf, 0); push_link(&mut buf, dz_off);
buf.push(0x01); buf.extend_from_slice(&[0u8; 3]); buf.extend_from_slice(&1u32.to_le_bytes()); buf.extend_from_slice(&original_len.to_le_bytes());
out.write_all(&buf)?;
let mut buf = Vec::with_capacity(DZ_HEADER_SIZE as usize);
block_header(&mut buf, b"##DZ", DZ_HEADER_SIZE + data.len() as u64, 0);
buf.extend_from_slice(b"DT"); buf.push(*zip_type); buf.push(0); buf.extend_from_slice(&zip_parameter.to_le_bytes()); buf.extend_from_slice(&original_len.to_le_bytes());
buf.extend_from_slice(&(data.len() as u64).to_le_bytes());
out.write_all(&buf)?;
out.write_all(data)?;
}
}
Ok(())
}