use std::ffi::{c_char, c_int, CStr, CString};
use std::fmt;
use std::path::Path;
use std::time::Duration;
pub const NOFFT_EXTENSIONS: &[&str] = &["no_fft"];
pub const NADC_EXTENSIONS: &[&str] = &["nadc"];
pub const NFM_EXTENSIONS: &[&str] = &["nfm"];
pub const NFM_IMG_NONE: u8 = 0;
pub const NFM_IMG_PNG: u8 = 1;
pub const NFM_IMG_JPEG: u8 = 2;
pub const CODEC_EXTENSIONS: &[&str] = &["no_fft", "nadc"];
mod ffi {
use std::ffi::{c_char, c_int};
pub type SinkBegin = unsafe extern "C" fn(*mut std::ffi::c_void, u32, u16, usize) -> c_int;
pub type SinkWrite = unsafe extern "C" fn(*mut std::ffi::c_void, *const f32, usize) -> c_int;
extern "C" {
pub fn nova_codec_kind(path: *const c_char) -> c_int;
pub fn nova_nfm_read(
path: *const c_char,
payload: *mut *mut u8,
payload_len: *mut usize,
title: *mut *const c_char,
artist: *mut *const c_char,
album: *mut *const c_char,
img: *mut *mut u8,
img_len: *mut usize,
img_type: *mut c_int,
) -> c_int;
pub fn nova_nfm_free_meta(
title: *mut *const c_char,
artist: *mut *const c_char,
album: *mut *const c_char,
);
pub fn nova_nfm_write(
path: *const c_char,
payload: *const u8,
payload_len: usize,
title: *const c_char,
artist: *const c_char,
album: *const c_char,
img: *const u8,
img_len: usize,
img_type: c_int,
) -> c_int;
pub fn nova_info(
path: *const c_char,
rate: *mut u32,
channels: *mut u16,
frames: *mut u64,
) -> c_int;
pub fn nova_decode(
path: *const c_char,
samples: *mut *mut f32,
count: *mut usize,
rate: *mut u32,
channels: *mut u16,
) -> c_int;
pub fn nova_free(samples: *mut f32);
pub fn nova_err_str(code: c_int) -> *const c_char;
pub fn nova_decode_sink(
path: *const c_char,
user: *mut std::ffi::c_void,
begin: SinkBegin,
write: SinkWrite,
) -> c_int;
pub fn nova_gen_wav(path: *const c_char, seconds: f64, channels: u32, rate: u32) -> c_int;
pub fn nova_encode_nofft(
in_wav: *const c_char,
out: *const c_char,
frame_len: u32,
degree: u16,
) -> c_int;
pub fn nova_encode_nadc(
in_wav: *const c_char,
out: *const c_char,
bits: u16,
variant: u32,
) -> c_int;
pub fn nova_encode_nadc5_rate(
in_wav: *const c_char,
out: *const c_char,
bits: u16,
rate: f64,
) -> c_int;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CodecKind {
Polynomial,
Nfa,
Nfm,
}
impl CodecKind {
fn from_raw(raw: c_int) -> Option<Self> {
match raw {
0 => Some(CodecKind::Polynomial),
1 => Some(CodecKind::Nfa),
2 => Some(CodecKind::Nfm),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NadcVariant {
Nfa1,
Nfa2,
Nfa3,
Nfa4,
Nfa5,
Nfa6,
}
impl NadcVariant {
fn as_raw(self) -> u32 {
match self {
NadcVariant::Nfa1 => 1,
NadcVariant::Nfa2 => 2,
NadcVariant::Nfa3 => 3,
NadcVariant::Nfa4 => 4,
NadcVariant::Nfa5 => 5,
NadcVariant::Nfa6 => 6,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CodecError {
code: i32,
message: String,
}
impl CodecError {
pub fn code(&self) -> i32 {
self.code
}
}
impl fmt::Display for CodecError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.message)
}
}
impl std::error::Error for CodecError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CodecInfo {
pub sample_rate: u32,
pub channels: u16,
pub frames: u64,
}
impl CodecInfo {
pub fn duration(&self) -> Duration {
if self.sample_rate == 0 {
return Duration::ZERO;
}
Duration::from_secs_f64(self.frames as f64 / f64::from(self.sample_rate))
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Pcm {
pub samples: Vec<f32>,
pub sample_rate: u32,
pub channels: u16,
}
impl Pcm {
pub fn frames(&self) -> usize {
if self.channels == 0 {
0
} else {
self.samples.len() / self.channels as usize
}
}
pub fn duration(&self) -> Duration {
if self.sample_rate == 0 {
return Duration::ZERO;
}
Duration::from_secs_f64(self.frames() as f64 / f64::from(self.sample_rate))
}
}
pub fn probe(path: &Path) -> Option<CodecKind> {
let c_path = c_path(path).ok()?;
let raw = unsafe { ffi::nova_codec_kind(c_path.as_ptr()) };
CodecKind::from_raw(raw)
}
pub fn is_codec_file(path: &Path) -> bool {
probe(path).is_some()
}
pub fn info(path: &Path) -> Result<CodecInfo, CodecError> {
let c_path = c_path(path)?;
let mut rate = 0u32;
let mut channels = 0u16;
let mut frames = 0u64;
let code = unsafe { ffi::nova_info(c_path.as_ptr(), &mut rate, &mut channels, &mut frames) };
check(code)?;
Ok(CodecInfo {
sample_rate: rate,
channels,
frames,
})
}
pub fn decode(path: &Path) -> Result<Pcm, CodecError> {
let c_path = c_path(path)?;
let mut samples: *mut f32 = std::ptr::null_mut();
let mut count = 0usize;
let mut rate = 0u32;
let mut channels = 0u16;
let code = unsafe {
ffi::nova_decode(
c_path.as_ptr(),
&mut samples,
&mut count,
&mut rate,
&mut channels,
)
};
check(code)?;
let data = if samples.is_null() || count == 0 {
Vec::new()
} else {
unsafe { std::slice::from_raw_parts(samples, count).to_vec() }
};
unsafe { ffi::nova_free(samples) };
Ok(Pcm {
samples: data,
sample_rate: rate,
channels,
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct StreamFormat {
pub sample_rate: u32,
pub channels: u16,
pub total_samples: usize,
}
impl StreamFormat {
pub fn frames(&self) -> usize {
if self.channels == 0 {
0
} else {
self.total_samples / self.channels as usize
}
}
pub fn duration(&self) -> Duration {
if self.sample_rate == 0 {
return Duration::ZERO;
}
Duration::from_secs_f64(self.frames() as f64 / f64::from(self.sample_rate))
}
}
pub fn decode_streaming<B, W>(path: &Path, begin: B, write: W) -> Result<StreamFormat, CodecError>
where
B: FnMut(StreamFormat),
W: FnMut(&[f32]) -> bool,
{
struct Callbacks<B: FnMut(StreamFormat), W: FnMut(&[f32]) -> bool> {
begin: B,
write: W,
format: StreamFormat,
aborted: bool,
}
unsafe extern "C" fn on_begin<B: FnMut(StreamFormat), W: FnMut(&[f32]) -> bool>(
user: *mut std::ffi::c_void,
sample_rate: u32,
channels: u16,
total_samples: usize,
) -> c_int {
let state = unsafe { &mut *(user as *mut Callbacks<B, W>) };
state.format = StreamFormat {
sample_rate,
channels,
total_samples,
};
(state.begin)(state.format);
0
}
unsafe extern "C" fn on_write<B: FnMut(StreamFormat), W: FnMut(&[f32]) -> bool>(
user: *mut std::ffi::c_void,
samples: *const f32,
count: usize,
) -> c_int {
let state = unsafe { &mut *(user as *mut Callbacks<B, W>) };
let slice = unsafe { std::slice::from_raw_parts(samples, count) };
if (state.write)(slice) {
0
} else {
state.aborted = true;
-1
}
}
let c_path = c_path(path)?;
let mut state = Callbacks {
begin,
write,
format: StreamFormat {
sample_rate: 0,
channels: 0,
total_samples: 0,
},
aborted: false,
};
let code = unsafe {
ffi::nova_decode_sink(
c_path.as_ptr(),
&mut state as *mut _ as *mut std::ffi::c_void,
on_begin::<B, W>,
on_write::<B, W>,
)
};
if state.aborted {
return Err(CodecError {
code: -1,
message: "decode stopped by the caller".to_string(),
});
}
check(code)?;
Ok(state.format)
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct NfmMeta {
pub title: Option<String>,
pub artist: Option<String>,
pub album: Option<String>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct NfmCover {
pub data: Vec<u8>,
pub img_type: u8,
}
#[derive(Debug)]
pub struct NfmFile {
pub payload: Vec<u8>,
pub meta: NfmMeta,
pub cover: Option<NfmCover>,
}
pub fn nfm_read(path: &Path) -> Result<NfmFile, CodecError> {
match nfm_read_current(path) {
Ok(file) => Ok(file),
Err(err) => {
let data = match std::fs::read(path) {
Ok(data) => data,
Err(_) => return Err(err),
};
match nfm_read_legacy(&data) {
Some(file) => Ok(file),
None => Err(err),
}
}
}
}
fn nfm_read_current(path: &Path) -> Result<NfmFile, CodecError> {
let c_path = c_path(path)?;
let mut payload: *mut u8 = std::ptr::null_mut();
let mut payload_len = 0usize;
let mut title: *const c_char = std::ptr::null();
let mut artist: *const c_char = std::ptr::null();
let mut album: *const c_char = std::ptr::null();
let mut img: *mut u8 = std::ptr::null_mut();
let mut img_len = 0usize;
let mut img_type: c_int = 0;
let code = unsafe {
ffi::nova_nfm_read(
c_path.as_ptr(),
&mut payload,
&mut payload_len,
&mut title,
&mut artist,
&mut album,
&mut img,
&mut img_len,
&mut img_type,
)
};
check(code)?;
let payload_data = if payload.is_null() || payload_len == 0 {
Vec::new()
} else {
unsafe { std::slice::from_raw_parts(payload, payload_len).to_vec() }
};
unsafe { ffi::nova_free(payload as *mut f32) };
let cover = if img.is_null() || img_len == 0 {
None
} else {
let img_data = unsafe { std::slice::from_raw_parts(img, img_len).to_vec() };
unsafe { ffi::nova_free(img as *mut f32) };
Some(NfmCover {
data: img_data,
img_type: img_type as u8,
})
};
let meta = NfmMeta {
title: copy_meta_string(title),
artist: copy_meta_string(artist),
album: copy_meta_string(album),
};
unsafe { ffi::nova_nfm_free_meta(&mut title, &mut artist, &mut album) };
Ok(NfmFile {
payload: payload_data,
meta,
cover,
})
}
fn nfm_read_legacy(data: &[u8]) -> Option<NfmFile> {
if data.len() < 11 || &data[0..3] != b"nfm" {
return None;
}
let hlen = u64::from_le_bytes(data[3..11].try_into().ok()?) as usize;
let hend = 11usize.checked_add(hlen)?;
if hend > data.len() {
return None;
}
let mut title = None;
let mut artist = None;
let mut album = None;
let mut pos = 11usize;
while pos < hend {
if pos + 2 > hend {
break;
}
let name_len = u16::from_le_bytes([data[pos], data[pos + 1]]) as usize;
let name_start = pos + 2;
let value_len_start = name_start + name_len;
let value_start = value_len_start + 4;
if value_start > hend {
break;
}
let value_len =
u32::from_le_bytes(data[value_len_start..value_start].try_into().ok()?) as usize;
let value_end = value_start.checked_add(value_len)?;
if value_end > hend {
break;
}
let name = &data[name_start..value_len_start];
let value = String::from_utf8_lossy(&data[value_start..value_end]).into_owned();
match name {
b"title" if title.is_none() => title = Some(value),
b"artist" if artist.is_none() => artist = Some(value),
b"album" if album.is_none() => album = Some(value),
_ => {}
}
pos = value_end;
}
let mut cover = None;
if pos + 5 <= hend {
let img_type = data[pos];
let img_len = u32::from_le_bytes(data[pos + 1..pos + 5].try_into().ok()?) as usize;
let end = pos.checked_add(5 + img_len)?;
if (img_type == NFM_IMG_PNG || img_type == NFM_IMG_JPEG) && img_len > 0 && end <= hend {
cover = Some(NfmCover {
data: data[pos + 5..end].to_vec(),
img_type,
});
}
}
if cover.is_none() {
let mut i = 11usize;
while i < hend {
let is_png = i + 4 <= hend && &data[i..i + 4] == b"\x89PNG";
let is_jpeg = i + 2 <= hend && data[i] == 0xFF && data[i + 1] == 0xD8;
if is_png || is_jpeg {
cover = Some(NfmCover {
data: data[i..hend].to_vec(),
img_type: if is_png { NFM_IMG_PNG } else { NFM_IMG_JPEG },
});
break;
}
i += 1;
}
}
let payload = data[hend..].to_vec();
kind_from_bytes(&payload).ok()?;
Some(NfmFile {
payload,
meta: NfmMeta {
title,
artist,
album,
},
cover,
})
}
fn copy_meta_string(ptr: *const c_char) -> Option<String> {
if ptr.is_null() {
return None;
}
let s = unsafe { CStr::from_ptr(ptr) };
Some(s.to_string_lossy().into_owned())
}
pub fn nfm_write(
path: &Path,
payload: &[u8],
meta: &NfmMeta,
cover: Option<&NfmCover>,
) -> Result<(), CodecError> {
let c_path = c_path(path)?;
let title = opt_c_string(meta.title.as_deref())?;
let artist = opt_c_string(meta.artist.as_deref())?;
let album = opt_c_string(meta.album.as_deref())?;
let (img_ptr, img_len, img_type) = match cover {
Some(c) => (c.data.as_ptr(), c.data.len(), c.img_type as c_int),
None => (std::ptr::null(), 0, 0),
};
let code = unsafe {
ffi::nova_nfm_write(
c_path.as_ptr(),
payload.as_ptr(),
payload.len(),
title.as_ref().map_or(std::ptr::null(), |s| s.as_ptr()),
artist.as_ref().map_or(std::ptr::null(), |s| s.as_ptr()),
album.as_ref().map_or(std::ptr::null(), |s| s.as_ptr()),
img_ptr,
img_len,
img_type,
)
};
check(code)
}
fn opt_c_string(value: Option<&str>) -> Result<Option<CString>, CodecError> {
match value {
Some(s) => CString::new(s).map(Some).map_err(|_| CodecError {
code: -1,
message: "metadata contains a NUL byte".to_string(),
}),
None => Ok(None),
}
}
pub fn kind_from_bytes(buf: &[u8]) -> Result<CodecKind, CodecError> {
if buf.len() < 4 {
return Err(CodecError {
code: -1,
message: "file too small".to_string(),
});
}
if &buf[0..4] == b"NOFF" {
return Ok(CodecKind::Polynomial);
}
if &buf[0..3] == b"NFA" && (b'1'..=b'6').contains(&buf[3]) {
return Ok(CodecKind::Nfa);
}
if &buf[0..3] == b"nfm" {
return Ok(CodecKind::Nfm);
}
Err(CodecError {
code: -1,
message: "unknown magic".to_string(),
})
}
pub fn generate_wav(
path: &Path,
seconds: f64,
channels: u32,
sample_rate: u32,
) -> Result<(), CodecError> {
let out = c_path(path)?;
let code = unsafe { ffi::nova_gen_wav(out.as_ptr(), seconds, channels, sample_rate) };
check(code)
}
pub fn encode_nofft(wav: &Path, out: &Path, frame_len: u32, degree: u16) -> Result<(), CodecError> {
let input = c_path(wav)?;
let output = c_path(out)?;
let code =
unsafe { ffi::nova_encode_nofft(input.as_ptr(), output.as_ptr(), frame_len, degree) };
check(code)
}
pub fn encode_nadc(
wav: &Path,
out: &Path,
bits: u16,
variant: NadcVariant,
) -> Result<(), CodecError> {
let input = c_path(wav)?;
let output = c_path(out)?;
let code =
unsafe { ffi::nova_encode_nadc(input.as_ptr(), output.as_ptr(), bits, variant.as_raw()) };
check(code)
}
pub fn encode_nadc5_rate(wav: &Path, out: &Path, bits: u16, rate: f64) -> Result<(), CodecError> {
let input = c_path(wav)?;
let output = c_path(out)?;
let code = unsafe { ffi::nova_encode_nadc5_rate(input.as_ptr(), output.as_ptr(), bits, rate) };
check(code)
}
fn check(code: i32) -> Result<(), CodecError> {
if code == 0 {
return Ok(());
}
let message = {
let ptr = unsafe { ffi::nova_err_str(code) };
if ptr.is_null() {
format!("codec error {code}")
} else {
unsafe { CStr::from_ptr(ptr) }
.to_string_lossy()
.into_owned()
}
};
Err(CodecError { code, message })
}
fn c_path(path: &Path) -> Result<CString, CodecError> {
CString::new(path_bytes(path)).map_err(|_| CodecError {
code: -1,
message: format!("path contains a NUL byte: {}", path.display()),
})
}
#[cfg(unix)]
fn path_bytes(path: &Path) -> Vec<u8> {
use std::os::unix::ffi::OsStrExt;
path.as_os_str().as_bytes().to_vec()
}
#[cfg(not(unix))]
fn path_bytes(path: &Path) -> Vec<u8> {
path.to_string_lossy().into_owned().into_bytes()
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::{Path as StdPath, PathBuf};
fn tmpdir(tag: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!("nova-codec-{tag}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).expect("create temp dir");
dir
}
fn test_wav(dir: &StdPath) -> PathBuf {
let wav = dir.join("signal.wav");
generate_wav(&wav, 0.25, 2, 44_100).expect("generate test wav");
wav
}
fn assert_playable(pcm: &Pcm, expected_frames: u64) {
assert_eq!(pcm.channels, 2);
assert_eq!(pcm.sample_rate, 44_100);
assert_eq!(pcm.frames() as u64, expected_frames);
assert!(pcm.samples.iter().all(|s| s.is_finite()));
assert!(
pcm.samples.iter().any(|s| s.abs() > 1e-4),
"decoded audio is silent"
);
}
#[test]
fn polynomial_round_trip_probes_and_decodes() {
let dir = tmpdir("poly");
let wav = test_wav(&dir);
let out = dir.join("signal.poly.no_fft");
encode_nofft(&wav, &out, 256, 8).expect("encode");
assert_eq!(probe(&out), Some(CodecKind::Polynomial));
let head = info(&out).expect("header info");
assert_eq!(head.channels, 2);
assert_eq!(head.sample_rate, 44_100);
assert!(head.frames > 0);
let pcm = decode(&out).expect("decode");
assert_playable(&pcm, head.frames);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn every_nadc_variant_round_trips() {
let dir = tmpdir("nadc");
let wav = test_wav(&dir);
for variant in [
NadcVariant::Nfa1,
NadcVariant::Nfa2,
NadcVariant::Nfa3,
NadcVariant::Nfa4,
NadcVariant::Nfa5,
NadcVariant::Nfa6,
] {
let out = dir.join(format!("signal.{}.nadc", variant.as_raw()));
encode_nadc(&wav, &out, 6, variant).expect("encode");
assert_eq!(probe(&out), Some(CodecKind::Nfa));
let head = info(&out).expect("header info");
assert_eq!(head.channels, 2);
assert_eq!(head.sample_rate, 44_100);
let pcm = decode(&out).expect("decode");
assert_playable(&pcm, head.frames);
}
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn nfm_container_round_trips() {
let dir = tmpdir("nfm");
let wav = test_wav(&dir);
let poly = dir.join("signal.poly.no_fft");
encode_nofft(&wav, &poly, 256, 8).expect("encode poly");
let nfa = dir.join("signal.nfa1.nadc");
encode_nadc(&wav, &nfa, 6, NadcVariant::Nfa1).expect("encode nfa");
let meta = NfmMeta {
title: Some("Round Trip".to_string()),
artist: Some("Nova".to_string()),
album: None,
};
let cover = NfmCover {
data: {
let mut png = vec![0x89, b'P', b'N', b'G', 0x0d, 0x0a, 0x1a, 0x0a];
png.extend_from_slice(b"not-a-real-png");
png
},
img_type: NFM_IMG_PNG,
};
for (name, payload_path) in [("poly.nfm", &poly), ("nfa.nfm", &nfa)] {
let payload = std::fs::read(payload_path).expect("read payload");
let nfm = dir.join(name);
nfm_write(&nfm, &payload, &meta, Some(&cover)).expect("nfm_write");
assert_eq!(probe(&nfm), Some(CodecKind::Nfm));
let read = nfm_read(&nfm).expect("nfm_read");
assert_eq!(read.payload, payload);
assert_eq!(read.meta, meta);
let got_cover = read.cover.expect("cover present");
assert_eq!(got_cover.img_type, NFM_IMG_PNG);
assert_eq!(got_cover.data, cover.data);
}
let _ = std::fs::remove_dir_all(&dir);
}
fn write_legacy_nfm(
path: &StdPath,
fields: &[(&str, &str)],
image: Option<&[u8]>,
payload: &[u8],
) {
use std::io::Write as _;
let mut header: Vec<u8> = Vec::new();
for (name, value) in fields {
header.extend_from_slice(&(name.len() as u16).to_le_bytes());
header.extend_from_slice(name.as_bytes());
header.extend_from_slice(&(value.len() as u32).to_le_bytes());
header.extend_from_slice(value.as_bytes());
}
if let Some(image) = image {
header.push(NFM_IMG_PNG);
header.extend_from_slice(&(image.len() as u32).to_le_bytes());
header.extend_from_slice(image);
}
let mut out: Vec<u8> = b"nfm".to_vec();
out.extend_from_slice(&(header.len() as u64).to_le_bytes());
out.extend_from_slice(&header);
out.extend_from_slice(payload);
let mut file = std::fs::File::create(path).expect("create legacy nfm");
file.write_all(&out).expect("write legacy nfm");
}
#[test]
fn legacy_nfm_files_without_image_block_are_read() {
let dir = tmpdir("nfm_legacy");
let wav = test_wav(&dir);
let nfa = dir.join("signal.nfa1.nadc");
encode_nadc(&wav, &nfa, 6, NadcVariant::Nfa1).expect("encode");
let payload = std::fs::read(&nfa).expect("read payload");
let bare = dir.join("bare.nfm");
write_legacy_nfm(&bare, &[], None, &payload);
let read = nfm_read(&bare).expect("read bare legacy");
assert_eq!(read.payload, payload);
assert_eq!(read.meta, NfmMeta::default());
assert!(read.cover.is_none());
let png = {
let mut p = vec![0x89, b'P', b'N', b'G', 0x0d, 0x0a, 0x1a, 0x0a];
p.extend_from_slice(b"legacy-image");
p
};
let tagged = dir.join("tagged.nfm");
write_legacy_nfm(
&tagged,
&[("title", "Legacy"), ("artist", "Old Format")],
Some(&png),
&payload,
);
let read = nfm_read(&tagged).expect("read tagged legacy");
assert_eq!(read.payload, payload);
assert_eq!(read.meta.title.as_deref(), Some("Legacy"));
assert_eq!(read.meta.artist.as_deref(), Some("Old Format"));
let cover = read.cover.expect("legacy cover");
assert_eq!(cover.img_type, NFM_IMG_PNG);
assert_eq!(cover.data, png);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn nfm_without_metadata_or_cover_round_trips() {
let dir = tmpdir("nfm_bare");
let wav = test_wav(&dir);
let nfa = dir.join("signal.nfa2.nadc");
encode_nadc(&wav, &nfa, 6, NadcVariant::Nfa2).expect("encode");
let payload = std::fs::read(&nfa).expect("read");
let nfm = dir.join("bare.nfm");
nfm_write(&nfm, &payload, &NfmMeta::default(), None).expect("nfm_write");
let read = nfm_read(&nfm).expect("nfm_read");
assert_eq!(read.payload, payload);
assert_eq!(read.meta, NfmMeta::default());
assert!(read.cover.is_none());
let kind = kind_from_bytes(&read.payload).expect("inner kind");
assert_eq!(kind, CodecKind::Nfa);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn streaming_decode_matches_the_buffered_one() {
let dir = tmpdir("stream");
let wav = test_wav(&dir);
let nfa = dir.join("signal.nfa5.nadc");
encode_nadc(&wav, &nfa, 6, NadcVariant::Nfa5).expect("encode");
let poly = dir.join("signal.poly.no_fft");
encode_nofft(&wav, &poly, 256, 8).expect("encode poly");
for path in [&nfa, &poly] {
let buffered = decode(path).expect("buffered decode");
let mut format = None;
let mut streamed: Vec<f32> = Vec::new();
let mut chunks = 0usize;
let reported = decode_streaming(
path,
|f| format = Some(f),
|samples| {
chunks += 1;
streamed.extend_from_slice(samples);
true
},
)
.expect("streaming decode");
let format = format.expect("begin ran");
assert_eq!(format.sample_rate, buffered.sample_rate);
assert_eq!(format.channels, buffered.channels);
assert_eq!(format.total_samples, buffered.samples.len());
assert_eq!(format.frames(), buffered.frames());
assert_eq!(reported, format);
assert!(chunks > 0, "no chunks arrived");
assert_eq!(streamed, buffered.samples, "streamed samples differ");
}
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn streaming_decode_stops_when_the_writer_says_so() {
let dir = tmpdir("stream_stop");
let wav = test_wav(&dir);
let nfa = dir.join("signal.nfa1.nadc");
encode_nadc(&wav, &nfa, 6, NadcVariant::Nfa1).expect("encode");
let mut seen = 0usize;
let err = decode_streaming(
&nfa,
|_| {},
|samples| {
seen += samples.len();
false
},
)
.expect_err("writer refusal aborts the decode");
assert!(err.to_string().contains("stopped by the caller"));
assert!(seen > 0, "the first chunk should have arrived");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn non_codec_files_are_not_probed() {
let dir = tmpdir("junk");
let file = dir.join("notes.txt");
std::fs::write(&file, b"just some text").expect("write");
assert_eq!(probe(&file), None);
assert!(!is_codec_file(&file));
assert!(info(&file).is_err());
assert!(decode(&file).is_err());
assert_eq!(probe(&dir.join("nope.no_fft")), None);
let _ = std::fs::remove_dir_all(&dir);
}
}