use crate::signals::{self, REF_RATE};
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Class {
Speech,
Music,
Mixed,
Synthetic,
}
impl Class {
pub fn name(self) -> &'static str {
match self {
Class::Speech => "speech",
Class::Music => "music",
Class::Mixed => "mixed",
Class::Synthetic => "synthetic",
}
}
pub fn parse(text: &str) -> Option<Self> {
match text {
"speech" => Some(Class::Speech),
"music" => Some(Class::Music),
"mixed" => Some(Class::Mixed),
"synthetic" => Some(Class::Synthetic),
_ => None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Split {
Tune,
Holdout,
}
impl Split {
pub fn name(self) -> &'static str {
match self {
Split::Tune => "tune",
Split::Holdout => "holdout",
}
}
}
pub fn fnv1a64(bytes: &[u8]) -> u64 {
let mut h = 0xcbf2_9ce4_8422_2325u64;
for &b in bytes {
h ^= b as u64;
h = h.wrapping_mul(0x0000_0100_0000_01b3);
}
h
}
pub fn split_of(name: &str) -> Split {
if fnv1a64(name.as_bytes()).is_multiple_of(4) { Split::Holdout } else { Split::Tune }
}
pub type Make = Box<dyn Fn() -> Result<(usize, Vec<i16>), String> + Send + Sync>;
pub enum Source {
File { path: PathBuf, start: f64, duration: f64 },
Generated(Make),
}
pub struct ItemSpec {
pub name: String,
pub class: Class,
pub source: Source,
}
impl ItemSpec {
pub fn file(name: String, class: Class, path: PathBuf, start: f64, duration: f64) -> Self {
Self { name, class, source: Source::File { path, start, duration } }
}
pub fn synth(name: &str, class: Class, make: impl Fn() -> Vec<Vec<f64>> + Send + Sync + 'static) -> Self {
Self {
name: name.to_string(),
class,
source: Source::Generated(Box::new(move || {
let channels = make();
Ok((channels.len(), signals::finish(channels)))
})),
}
}
pub fn generated(name: &str, class: Class, make: Make) -> Self {
Self { name: name.to_string(), class, source: Source::Generated(make) }
}
}
pub struct Item {
pub name: String,
pub class: Class,
pub split: Split,
pub channels: usize,
pub reference: Vec<i16>,
moc_reference: OnceLock<Vec<f32>>,
}
impl Item {
pub fn new(name: String, class: Class, channels: usize, mut reference: Vec<i16>) -> Self {
let frames = reference.len() / channels / 12 * 12;
reference.truncate(frames * channels);
Self { split: split_of(&name), name, class, channels, reference, moc_reference: OnceLock::new() }
}
pub fn frames(&self) -> usize {
self.reference.len() / self.channels
}
pub fn seconds(&self) -> f64 {
self.frames() as f64 / REF_RATE as f64
}
pub fn moc_reference(&self) -> &[f32] {
self.moc_reference.get_or_init(|| {
let mono = self.mono_f64();
signals::Resampler::new(REF_RATE, 16_000).run(&mono).iter().map(|&v| v as f32).collect()
})
}
pub fn mono_f64(&self) -> Vec<f64> {
if self.channels == 1 {
self.reference.iter().map(|&s| s as f64 / 32768.0).collect()
} else {
self.reference
.as_chunks::<2>()
.0
.iter()
.map(|p| 0.5 * (p[0] as f64 + p[1] as f64) / 32768.0)
.collect()
}
}
}
pub fn load(spec: &ItemSpec) -> Result<Item, String> {
let (channels, samples) = match &spec.source {
Source::Generated(make) => make()?,
Source::File { path, start, duration } => {
let wav = signals::read_wav(path)?;
let mut samples = if wav.rate == REF_RATE {
wav.samples
} else {
signals::resample_interleaved(&wav.samples, wav.channels, wav.rate, REF_RATE)
};
let frames = samples.len() / wav.channels;
let first = ((start * REF_RATE as f64).round() as usize).min(frames);
let last = if *duration > 0.0 {
(first + (duration * REF_RATE as f64).round() as usize).min(frames)
} else {
frames
};
samples.truncate(last * wav.channels);
samples.drain(..first * wav.channels);
(wav.channels, samples.iter().map(|&v| signals::to_i16(v)).collect())
}
};
if !(1..=2).contains(&channels) {
return Err(format!("{}: {channels} channels", spec.name));
}
let item = Item::new(spec.name.clone(), spec.class, channels, samples);
if item.frames() < 4800 {
return Err(format!("{}: shorter than 0.1 s", spec.name));
}
Ok(item)
}
pub fn load_all(specs: &[ItemSpec], threads: usize) -> Result<Vec<Item>, String> {
let next = std::sync::atomic::AtomicUsize::new(0);
let slots: Vec<std::sync::Mutex<Option<Result<Item, String>>>> =
specs.iter().map(|_| std::sync::Mutex::new(None)).collect();
std::thread::scope(|scope| {
for _ in 0..threads.max(1).min(specs.len().max(1)) {
scope.spawn(|| {
loop {
let i = next.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
if i >= specs.len() {
break;
}
let item = load(&specs[i]);
*slots[i].lock().unwrap() = Some(item);
}
});
}
});
slots.into_iter().map(|s| s.into_inner().unwrap().expect("every item loaded")).collect()
}
pub fn corpus_root() -> PathBuf {
match std::env::var_os("OPUS1_CORPUS") {
Some(dir) if !dir.is_empty() => PathBuf::from(dir),
_ => Path::new(env!("CARGO_MANIFEST_DIR")).join("corpus"),
}
}
pub fn fixtures_dir() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("fixtures")
}
pub fn read_set_file(path: &Path, root: &Path) -> Result<Vec<ItemSpec>, String> {
let text = std::fs::read_to_string(path).map_err(|e| format!("{}: {e}", path.display()))?;
parse_set(&text, root).map_err(|e| format!("{}: {e}", path.display()))
}
pub fn parse_set(text: &str, root: &Path) -> Result<Vec<ItemSpec>, String> {
let mut specs = Vec::new();
for (n, line) in text.lines().enumerate() {
let line = line.split('#').next().unwrap_or("").trim();
if line.is_empty() {
continue;
}
let fields: Vec<&str> = line.split('\t').map(str::trim).collect();
let bad = |what: &str| format!("line {}: {what}", n + 1);
let name = fields[0];
let class = fields.get(1).map_or(Some(Class::Speech), |c| Class::parse(c)).ok_or_else(|| bad("unknown class"))?;
let number = |i: usize| -> Result<f64, String> {
fields.get(i).map_or(Ok(0.0), |v| v.parse::<f64>().map_err(|_| bad("bad number")))
};
let (start, duration) = (number(2)?, number(3)?);
let path = if Path::new(name).is_absolute() { PathBuf::from(name) } else { root.join(name) };
specs.push(ItemSpec::file(name.to_string(), class, path, start, duration));
}
Ok(specs)
}
fn wav_files(dir: &Path, out: &mut Vec<PathBuf>) -> Result<(), String> {
let entries = std::fs::read_dir(dir).map_err(|e| format!("{}: {e}", dir.display()))?;
let mut paths: Vec<PathBuf> = entries.flatten().map(|e| e.path()).collect();
paths.sort();
for path in paths {
if path.file_name().is_some_and(|n| n.to_string_lossy().starts_with('.')) {
continue;
}
if path.is_dir() {
wav_files(&path, out)?;
} else if path.extension().is_some_and(|e| e.eq_ignore_ascii_case("wav")) {
out.push(path);
}
}
Ok(())
}
pub fn corpus_index(dir: &Path, root: &Path, per_group: usize, class: Class) -> Result<String, String> {
let mut files = Vec::new();
wav_files(dir, &mut files)?;
let mut groups: BTreeMap<PathBuf, Vec<PathBuf>> = BTreeMap::new();
for f in files {
groups.entry(f.parent().map(Path::to_path_buf).unwrap_or_default()).or_default().push(f);
}
let mut text = format!(
"# Written by `cargo run --release --example eval -- corpus-index`.\n\
# {} group(s) under {}, {per_group} file(s) per group, evenly spaced.\n\
# path\tclass\tstart_s\tduration_s\n",
groups.len(),
dir.strip_prefix(root).unwrap_or(dir).display()
);
for files in groups.values() {
let take = per_group.min(files.len());
for k in 0..take {
let f = &files[k * files.len() / take];
let shown = f.strip_prefix(root).unwrap_or(f);
let shown: Vec<String> = shown.components().map(|c| c.as_os_str().to_string_lossy().into_owned()).collect();
text.push_str(&format!("{}\t{}\t0\t0\n", shown.join("/"), class.name()));
}
}
Ok(text)
}
pub fn items_hash(items: &[Item]) -> u64 {
let mut bytes = Vec::new();
for item in items {
bytes.extend_from_slice(item.name.as_bytes());
bytes.push(0);
bytes.extend_from_slice(&fnv1a64(&item.reference.iter().flat_map(|s| s.to_le_bytes()).collect::<Vec<u8>>()).to_le_bytes());
}
fnv1a64(&bytes)
}