use std::path::Path;
use dspk_core::{DspError, DspResult, RecordingSource};
use super::format::Format;
use super::sources::SourceEntry;
use crate::registry::formats;
pub fn detect(path: &Path) -> DspResult<&'static dyn Format> {
detect_in(formats(), path)
}
pub fn detect_in(formats: &[&'static dyn Format], path: &Path) -> DspResult<&'static dyn Format> {
if !path.exists() {
return Err(DspError::Io(format!("{} does not exist", path.display())));
}
formats.iter().copied().find(|f| f.detect(path)).ok_or_else(|| {
let names: Vec<&str> = formats.iter().map(|f| f.name()).collect();
DspError::UnsupportedFormat(format!(
"cannot tell the format of {} (compiled-in formats: {}; a raw binary file needs a JSON sidecar)",
path.display(),
names.join(", ")
))
})
}
pub fn open(path: &Path) -> DspResult<Box<dyn RecordingSource>> {
detect(path)?.open_default(path)
}
pub fn sources(path: &Path) -> DspResult<Vec<SourceEntry>> {
detect(path)?.sources(path)
}
pub fn open_source(path: &Path, id: &str) -> DspResult<Box<dyn RecordingSource>> {
detect(path)?.open(path, id)
}
#[cfg(test)]
mod tests {
use super::*;
use dspk_core::{MemoryOrder, SampleFormat};
#[cfg(feature = "zarr")]
use crate::core::CachedRecording;
use crate::generic::raw::write_raw;
#[cfg(feature = "zarr")]
use crate::generic::zarr_traces::write_zarr;
use crate::test_support::{assert_native_matches, assert_stored_matches};
use dspk_core::MemoryRecording;
fn source(channels: usize) -> MemoryRecording {
const SAMPLES: usize = 3_000;
let data = (0..channels)
.flat_map(|c| (0..SAMPLES).map(move |t| 100.0 * ((t as f32) * 0.01 * (c + 1) as f32).sin()))
.collect();
MemoryRecording::new("source", data, channels, 30_000.0).unwrap()
}
#[test]
fn test_open_detects_formats() {
let dir = std::env::temp_dir().join(format!("dspk_io_open_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let src = source(4);
let bin = dir.join("rec.bin");
write_raw(&src, &bin, SampleFormat::I16, MemoryOrder::TimeMajor, 0.25, 1000, |_, _| {}).unwrap();
let rec = open(&bin).unwrap();
assert_eq!(rec.info().format, SampleFormat::I16);
assert_eq!(rec.info().samples, src.info().samples);
#[cfg(feature = "zarr")]
{
let z = dir.join("rec.zarr");
write_zarr(&src, &z, &crate::ZarrOptions { chunk_samples: 1000, ..Default::default() }, |_, _| {}).unwrap();
assert_eq!(open(&z).unwrap().info().channel_count(), 4);
}
std::fs::write(dir.join("other.dat"), [0u8; 8]).unwrap();
assert!(matches!(open(&dir.join("other.dat")), Err(DspError::UnsupportedFormat(_))));
assert!(open(&dir.join("missing.bin")).is_err());
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn test_single_recording_is_one_source() {
let dir = std::env::temp_dir().join(format!("dspk_io_src_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let src = source(3);
let bin = dir.join("rec.bin");
write_raw(&src, &bin, SampleFormat::F32, MemoryOrder::ChannelMajor, 1.0, 1000, |_, _| {}).unwrap();
let list = sources(&bin).unwrap();
assert_eq!(list.len(), 1);
assert_eq!((list[0].id.as_str(), list[0].channels), (crate::core::sources::MAIN, 3));
assert_eq!(list[0].unit, src.info().channels[0].unit);
assert_eq!(open_source(&bin, crate::core::sources::MAIN).unwrap().info().channel_count(), 3);
assert!(open_source(&bin, "nope").is_err());
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn stored_reads_match_scaled_reads() {
let dir = std::env::temp_dir().join(format!("dspk_io_stored_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let src = source(5);
let channels = [4usize, 0, 2];
for (name, format, order, gain) in [
("tm_i16.bin", SampleFormat::I16, MemoryOrder::TimeMajor, 0.25),
("cm_i16.bin", SampleFormat::I16, MemoryOrder::ChannelMajor, 0.5),
("cm_f32.bin", SampleFormat::F32, MemoryOrder::ChannelMajor, 1.0),
("tm_i32.bin", SampleFormat::I32, MemoryOrder::TimeMajor, 0.01),
] {
let bin = dir.join(name);
write_raw(&src, &bin, format, order, gain, 700, |_, _| {}).unwrap();
let rec = open(&bin).unwrap();
assert_stored_matches(rec.as_ref(), &channels, 100..1_900);
assert_native_matches(rec.as_ref(), 100..1_900);
let parent: std::sync::Arc<dyn RecordingSource> = std::sync::Arc::from(rec);
let sliced = dspk_core::SlicedRecording::new(parent, 50..2_000, Some(vec![3, 1, 4])).unwrap();
assert_stored_matches(&sliced, &[2, 0], 10..900);
}
assert_stored_matches(&src, &channels, 0..500);
#[cfg(feature = "zarr")]
{
let z = dir.join("rec.zarr");
write_zarr(&src, &z, &crate::ZarrOptions { chunk_samples: 700, ..Default::default() }, |_, _| {}).unwrap();
assert_stored_matches(open(&z).unwrap().as_ref(), &channels, 100..1_900);
assert_native_matches(open(&z).unwrap().as_ref(), 100..1_900);
let cached = CachedRecording::wrap(open(&z).unwrap(), 1 << 20);
assert_native_matches(cached.as_ref(), 100..1_900);
assert_stored_matches(cached.as_ref(), &channels, 100..1_900);
}
std::fs::remove_dir_all(&dir).unwrap();
}
}