灵符lingfu

ling-lang

↓ 0 downloads

Ling - The Omniglot Systems Language

Keywords systems, polyglot, ai, crypto, game
lingfu install ling-lang
LicenseApache-2.0 OR MIT
AuthorsLing Lin, Sanny Lin
READMECode / FilesVersionsDependency GraphDependentsSecurityBuildsSettings

Code / Files (995)

crates/ling-body/src/kit.rs
//! `BodyKit` is the stateful façade the runtime exposes to Ling programs.
//! Everything crossing the boundary is a number, a string or a flat float list.

use crate::analysis;
use crate::codec;
use crate::curves::{self, CreaseCfg, Curve};
use crate::detail::{self, DetailCfg};
use crate::export;
use crate::humanoid::Humanoid;
use crate::mesh::{Mesh, Tris};
use crate::mesher;
use crate::model::{self, Model};
use crate::params::{Params, Source, Waveform, DEFS};
use crate::signature::Signature;
use crate::sim::{build_rest, Rest, Sim};
use serde_json::json;

type Joint = (String, [f32; 3]);

/// Everything detail synthesis needs from the simulation that produced the mesh.
struct DetailCtx {
    base: Mesh,
    scalar: Vec<f32>,
    phi: Vec<f32>,
    grid: crate::Grid,
}

#[derive(Default)]
pub struct BodyKit {
    pub params: Params,
    models: Vec<Model>,
    active_model: Option<usize>,
    rest: Option<Rest>,
    sim: Option<Sim>,
    mesh: Option<Mesh>,
    /// Outward skin displacement per mesh vertex in millimetres; empty unless the mesh came from a simulation.
    push: Vec<f32>,
    /// Mesh recovered verbatim from an imported WAV.
    recovered: bool,
    audio: Option<(Vec<f32>, Vec<f32>, u32)>,
    notes: Vec<String>,
    creases: Vec<Curve>,
    detail: Option<DetailCtx>,
    listen: Option<Signature>,
    listen_spectrum: Vec<f32>,
}

impl BodyKit {
    pub fn new() -> BodyKit {
        BodyKit::default()
    }

    pub fn param_count(&self) -> usize {
        DEFS.len()
    }

    pub fn param_def(&self, i: usize) -> Option<(&'static str, f64, f64, f64, &'static str)> {
        DEFS.get(i).copied()
    }

    /// Forgets crease curves and the simulation snapshot they were found on.
    fn drop_detail(&mut self) {
        self.creases.clear();
        self.detail = None;
    }

    fn invalidate(&mut self) {
        self.rest = None;
        self.sim = None;
    }

    pub fn param_set(&mut self, name: &str, v: f64) -> bool {
        let ok = self.params.set(name, v);
        if ok {
            match Params::def(name).map(|d| d.4) {
                Some("body") | Some("sim") => self.invalidate(),
                Some("medium") | Some("mic") | Some("membrane") => {
                    if matches!(name, "c_air" | "c_body") {
                        self.invalidate();
                    } else {
                        self.sim = None;
                    }
                },
                _ => {},
            }
        }
        ok
    }

    pub fn notes(&self) -> &[String] {
        &self.notes
    }

    pub fn models_loaded(&self) -> usize {
        self.models.len()
    }

    pub fn load_model(&mut self, path: &str) -> Result<usize, String> {
        let m = Model::load(path)?;
        self.models.push(m);
        Ok(self.models.len() - 1)
    }

    pub fn model_parts(&self, h: usize) -> Vec<(String, usize, bool)> {
        self.models
            .get(h)
            .map(|m| m.parts.iter().map(|p| (p.name.clone(), p.tris.len(), p.enabled)).collect())
            .unwrap_or_default()
    }

    pub fn set_part(&mut self, h: usize, part: usize, on: bool) {
        if let Some(p) = self.models.get_mut(h).and_then(|m| m.parts.get_mut(part)) {
            p.enabled = on;
            if self.active_model == Some(h) {
                self.invalidate();
            }
        }
    }

    /// Selects the loaded model as the body, or the parametric scaffold for `None`.
    pub fn use_model(&mut self, h: Option<usize>) {
        self.active_model = h.filter(|&i| i < self.models.len());
        self.invalidate();
        self.drop_detail();
        self.recovered = false;
    }

    pub fn active_model(&self) -> Option<usize> {
        self.active_model
    }

    fn body_tris(&self) -> Option<(Tris, Vec<Joint>)> {
        let m = self.models.get(self.active_model?)?;
        let mut t = m.tris();
        let mut j = m.joints.clone();
        model::normalize(&mut t, &mut j, Some(self.params.getf("height")));
        Some((t, j))
    }

    pub fn build_rest(&mut self) -> Result<(), String> {
        if self.rest.is_some() {
            return Ok(());
        }
        let tris = self.body_tris().map(|b| b.0);
        self.rest = Some(build_rest(&self.params, tris.as_ref())?);
        Ok(())
    }

    /// Joint hierarchy of the scaffold fitted to the current body.
    pub fn joints(&self) -> Vec<(String, [f32; 3])> {
        let rig = match &self.rest {
            Some(r) => r.rig.clone(),
            None => match self.body_tris().and_then(|b| b.0.bounds()) {
                Some((lo, hi)) => Humanoid::fit(lo, hi),
                None => Humanoid::new(
                    self.params.getf("height"),
                    self.params.getf("build"),
                    self.params.getf("arm_span"),
                    self.params.getf("head_scale"),
                ),
            },
        };
        rig.joints.iter().map(|j| (j.name.clone(), j.pos)).collect()
    }

    pub fn joint_parent(&self, i: usize) -> i64 {
        let rig = Humanoid::new(1.0, 1.0, 1.0, 1.0);
        rig.joints.get(i).and_then(|j| j.parent).map_or(-1, |p| p as i64)
    }

    pub fn grid_info(&self) -> Option<String> {
        let r = self.rest.as_ref()?;
        let g = r.grid;
        Some(format!("{}x{}x{} @ {:.1} mm", g.nx, g.ny, g.nz, g.dx * 1000.0))
    }

    pub fn source_count(&self) -> usize {
        self.params.sources.len()
    }

    pub fn source_add(&mut self, pos: [f32; 3], wave: i64, freq: f32) -> usize {
        let n = self.params.sources.len();
        self.params.sources.push(Source::new(&format!("src{n}"), pos, Waveform::from_index(wave), freq.max(1.0)));
        self.sim = None;
        n
    }

    pub fn source_rename(&mut self, i: usize, name: &str) {
        if let Some(s) = self.params.sources.get_mut(i) {
            s.name = name.to_string();
        }
    }

    pub fn source_remove(&mut self, i: usize) {
        if i < self.params.sources.len() {
            self.params.sources.remove(i);
            self.sim = None;
        }
    }

    pub fn source_clear(&mut self) {
        self.params.sources.clear();
        self.sim = None;
    }

    pub fn source_get(&self, i: usize, field: &str) -> f64 {
        let Some(s) = self.params.sources.get(i) else { return 0.0 };
        match field {
            "x" => s.pos[0] as f64,
            "y" => s.pos[1] as f64,
            "z" => s.pos[2] as f64,
            "wave" => s.wave.index() as f64,
            "freq" => s.freq as f64,
            "amp" => s.amp as f64,
            "phase" => s.phase as f64,
            "start" => s.start as f64,
            "dur" => s.dur as f64,
            _ => 0.0,
        }
    }

    pub fn source_set(&mut self, i: usize, field: &str, v: f64) -> bool {
        let Some(s) = self.params.sources.get_mut(i) else { return false };
        let v = if v.is_finite() { v as f32 } else { 0.0 };
        match field {
            "x" => s.pos[0] = v,
            "y" => s.pos[1] = v,
            "z" => s.pos[2] = v,
            "wave" => s.wave = Waveform::from_index(v as i64),
            "freq" => s.freq = v.clamp(1.0, 40000.0),
            "amp" => s.amp = v,
            "phase" => s.phase = v,
            "start" => s.start = v.max(0.0),
            "dur" => s.dur = v.max(0.0),
            _ => return false,
        }
        self.sim = None;
        true
    }

    pub fn source_name(&self, i: usize) -> String {
        self.params.sources.get(i).map(|s| s.name.clone()).unwrap_or_default()
    }

    /// Voxelises the body and extracts its rest shape.
    pub fn preview_rest(&mut self) -> Result<(), String> {
        self.build_rest()?;
        let r = self.rest.as_ref().ok_or("rest body missing")?;
        self.mesh = Some(self.build_mesh(&r.phi0, &r.grid));
        self.push.clear();
        self.drop_detail();
        self.recovered = false;
        Ok(())
    }

    fn build_mesh(&self, phi: &[f32], g: &crate::Grid) -> Mesh {
        mesher::build(phi, g, self.params.getf("iso"), self.params.get("smooth") as usize, self.params.getf("decimate"))
    }

    pub fn sim_begin(&mut self) -> Result<usize, String> {
        self.build_rest()?;
        let rest = self.rest.as_ref().ok_or("rest body missing")?;
        let s = Sim::begin(&self.params, rest)?;
        let total = s.total;
        self.sim = Some(s);
        self.audio = None;
        self.recovered = false;
        Ok(total)
    }

    /// Runs the simulation for about `budget_ms`; returns `(steps_done, total)`.
    pub fn sim_advance(&mut self, budget_ms: f32) -> (usize, usize) {
        match self.sim.as_mut() {
            Some(s) => {
                s.advance(budget_ms);
                (s.steps_done(), s.total)
            },
            None => (0, 0),
        }
    }

    pub fn sim_active(&self) -> bool {
        self.sim.is_some()
    }

    pub fn sim_done(&self) -> bool {
        self.sim.as_ref().is_some_and(Sim::done)
    }

    /// Extracts the membrane's current zero level set as the working mesh.
    pub fn mesh_from_sim(&mut self) -> Result<(), String> {
        let s = self.sim.as_ref().ok_or("no simulation running")?;
        let m = self.build_mesh(&s.membrane.phi, &s.membrane.grid);
        let g = s.membrane.grid;
        let (rest, now) = (s.membrane.rest(), &s.membrane.phi);
        self.push = m
            .pos
            .as_chunks::<3>().0.iter()
            .map(|p| {
                let l = g.lattice([p[0], p[1], p[2]]);
                (g.sample(rest, l) - g.sample(now, l)) * 1000.0
            })
            .collect();
        let done = s.done();
        self.mesh = Some(m);
        self.drop_detail();
        self.recovered = false;
        if done {
            self.capture_audio();
        }
        Ok(())
    }

    fn capture_audio(&mut self) {
        if let Some(s) = &self.sim {
            if s.wave.mic_signal.len() == 2 {
                let rate = self.params.get("sr").round() as u32;
                self.audio = Some((s.wave.mic_signal[0].clone(), s.wave.mic_signal[1].clone(), rate));
            }
        }
    }

    pub fn displacement_mm(&self) -> (f32, f32) {
        self.sim.as_ref().map_or((0.0, 0.0), |s| {
            let (a, b) = s.membrane.displacement_range();
            (a * 1000.0, b * 1000.0)
        })
    }

    pub fn slice(&self, axis: usize, frac: f32) -> Option<(usize, usize, Vec<f32>)> {
        self.sim.as_ref().map(|s| s.slice(axis, frac))
    }

    pub fn mic_tail(&self, count: usize) -> Vec<f32> {
        self.sim.as_ref().and_then(|s| s.wave.mic_signal.first()).map_or_else(Vec::new, |m| {
            m[m.len().saturating_sub(count)..].to_vec()
        })
    }

    /// Finds crease curves on the current mesh from the simulated acoustic field and
    /// stores the snapshot needed to carve them later.
    pub fn crease_detect(&mut self) -> Result<String, String> {
        let s = self.sim.as_ref().ok_or("simulate first: creases come from the pressure field")?;
        let base = match &self.detail {
            Some(d) => d.base.clone(),
            None => self.mesh.clone().ok_or("no mesh; simulate first")?,
        };
        let pts: Vec<[f32; 3]> = base.pos.as_chunks::<3>().0.to_vec();
        let nodal = self.params.get("crease_mode") >= 0.5;
        let mut field = s.surface_field(&pts, !nodal);
        smooth_scalar(&base, &mut field, 4);
        let cfg = CreaseCfg {
            levels: self.params.get("crease_levels").round() as usize,
            tolerance: self.params.getf("crease_tol") / 1000.0,
            min_length: self.params.getf("crease_min") / 1000.0,
        };
        self.creases = curves::detect(&base, &field, &cfg, nodal);
        self.detail = Some(DetailCtx { base, scalar: field, phi: s.membrane.phi.clone(), grid: s.membrane.grid });
        let segs: usize = self.creases.iter().map(|c| c.segs.len()).sum();
        Ok(format!("{} crease curves, {} Bezier segments", self.creases.len(), segs))
    }

    /// Subdivides the mesh and carves grooves and noise along the detected creases.
    pub fn crease_apply(&mut self) -> Result<String, String> {
        let ctx = self.detail.as_ref().ok_or("find creases first")?;
        let cfg = DetailCfg {
            levels: self.params.get("detail_level").round() as usize,
            depth: self.params.getf("crease_depth") / 1000.0,
            width: self.params.getf("crease_width") / 1000.0,
            noise_amp: self.params.getf("noise_amp") / 1000.0,
            noise_freq: self.params.getf("noise_freq"),
        };
        let m = detail::build(&ctx.base, &ctx.scalar, &ctx.phi, &ctx.grid, &self.creases, &cfg);
        let tris = m.tri_count();
        self.mesh = Some(m);
        self.push.clear();
        self.recovered = false;
        Ok(format!("detail added: {tris} triangles"))
    }

    /// Returns to the smooth mesh the creases were found on.
    pub fn detail_reset(&mut self) -> Result<String, String> {
        let ctx = self.detail.as_ref().ok_or("no detail to remove")?;
        self.mesh = Some(ctx.base.clone());
        self.push.clear();
        Ok("smooth mesh restored".into())
    }

    pub fn crease_clear(&mut self) {
        if self.detail.is_some() {
            let _ = self.detail_reset();
        }
        self.drop_detail();
    }

    pub fn creases(&self) -> &[Curve] {
        &self.creases
    }

    pub fn crease_export(&self, path: &str) -> Result<String, String> {
        if self.creases.is_empty() {
            return Err("no creases to export; find creases first".into());
        }
        let lower = path.to_lowercase();
        let text = if lower.ends_with(".py") {
            curves::to_blender_script(&self.creases)
        } else if lower.ends_with(".json") {
            curves::to_json(&self.creases)
        } else {
            return Err("crease export must be .json or .py".into());
        };
        export::write(path, text.as_bytes())?;
        Ok(format!("{} curves → {path}", self.creases.len()))
    }

    pub fn listen_begin(&mut self) -> Result<usize, String> {
        let mesh = self.mesh.as_ref().ok_or("no mesh to listen to; preview a body or simulate first")?;
        let s = Signature::begin(mesh, &self.params)?;
        let total = s.total();
        self.listen = Some(s);
        Ok(total)
    }

    /// Returns `(steps_done, total)`.
    pub fn listen_advance(&mut self, budget_ms: f32) -> (usize, usize) {
        match self.listen.as_mut() {
            Some(s) => {
                s.advance(budget_ms);
                (s.steps_done(), s.total())
            },
            None => (0, 0),
        }
    }

    pub fn listen_done(&self) -> bool {
        self.listen.as_ref().is_some_and(Signature::done)
    }

    /// Renders the finished signature to a WAV and keeps its spectrum and resonance notes.
    pub fn listen_finish(&mut self, path: &str) -> Result<String, String> {
        let s = self.listen.as_ref().ok_or("start listening first")?;
        let out = s.render(path)?;
        self.listen_spectrum = out.spectrum;
        self.notes = out.notes;
        Ok(format!("{:.2} s signature → {path}", out.left.len() as f32 / out.rate as f32))
    }

    pub fn listen_spectrum(&self) -> &[f32] {
        &self.listen_spectrum
    }

    pub fn mesh(&self) -> Option<&Mesh> {
        self.mesh.as_ref()
    }

    /// Per-vertex outward skin displacement in millimetres, or an empty slice.
    pub fn mesh_push(&self) -> &[f32] {
        match &self.mesh {
            Some(m) if self.push.len() == m.vert_count() => &self.push,
            _ => &[],
        }
    }

    pub fn mesh_info(&self) -> (usize, usize, bool) {
        self.mesh.as_ref().map_or((0, 0, false), |m| (m.vert_count(), m.tri_count(), m.is_watertight()))
    }

    pub fn recovered_from_wav(&self) -> bool {
        self.recovered
    }

    pub fn body_bounds(&self) -> Option<([f32; 3], [f32; 3])> {
        self.mesh.as_ref().and_then(Mesh::bounds)
    }

    pub fn export_mesh(&self, path: &str) -> Result<String, String> {
        let m = self.mesh.as_ref().ok_or("no mesh to export; simulate or preview first")?;
        let lower = path.to_lowercase();
        if lower.ends_with(".obj") {
            export::write(path, export::obj_text(m).as_bytes())?;
        } else if lower.ends_with(".stl") {
            export::write(path, &export::stl_bytes(m))?;
        } else if lower.ends_with(".glb") {
            let rig = self.fitted_rig(m);
            export::write(path, &export::glb_bytes(m, Some(&rig)))?;
        } else {
            return Err("export format must be .obj, .stl or .glb".into());
        }
        Ok(format!("{} triangles → {path}", m.tri_count()))
    }

    fn fitted_rig(&self, m: &Mesh) -> Humanoid {
        match m.bounds() {
            Some((lo, hi)) => Humanoid::fit(lo, hi),
            None => Humanoid::new(1.7, 1.0, 1.0, 1.0),
        }
    }

    pub fn wav_export(&self, path: &str) -> Result<String, String> {
        let (l, r, rate) = self.audio.as_ref().ok_or("run the simulation to completion first")?;
        let repeat = self.params.get("wav_repeat").round().max(1.0) as usize;
        let period = l.len() + (self.params.get("wav_gap") * *rate as f64).round() as usize;
        let tile = |x: &[f32]| {
            let mut out = Vec::with_capacity(period * repeat);
            for _ in 0..repeat {
                out.extend_from_slice(x);
                out.resize(out.len() + period - x.len(), 0.0);
            }
            out
        };
        let (l, r) = (tile(l), tile(r));
        let bytes = codec::encode_body(
            &self.params,
            self.mesh.as_ref(),
            &l,
            &r,
            *rate,
            self.params.get("wav_bits").round() as u32,
            self.params.getf("wav_gain"),
        );
        export::write(path, &bytes)?;
        Ok(format!("{:.2} s, {} KB → {path}", l.len() as f32 / *rate as f32, bytes.len() / 1024))
    }

    /// Loads a WAV. Embedded parameters and mesh are used verbatim unless
    /// `analyze_only` is set, in which case the audio alone is analysed.
    pub fn wav_import(&mut self, path: &str, analyze_only: bool) -> Result<String, String> {
        let bytes = std::fs::read(path).map_err(|e| format!("{path}: {e}"))?;
        let d = codec::decode_body(&bytes)?;
        self.notes.clear();
        self.invalidate();
        let seconds = d.left.len() as f32 / d.rate.max(1) as f32;
        self.audio = Some((d.left.clone(), d.right.clone(), d.rate));
        self.drop_detail();
        if let (Some(p), false) = (&d.params, analyze_only) {
            self.params = p.clone();
            self.recovered = d.mesh.is_some();
            self.mesh = d.mesh;
            self.notes.push("parameters read from the file's LBDY chunk".into());
            if self.recovered {
                self.notes.push("mesh restored from the LBMS chunk".into());
            }
            return Ok(format!("exact recipe, {} sources, {:.2} s", self.params.sources.len(), seconds));
        }
        let a = analysis::analyze(&d.left, &d.right, d.rate, &self.params)?;
        self.params = a.params;
        self.notes = a.notes;
        self.recovered = false;
        self.mesh = None;
        Ok(format!("estimated from audio, {} sources, {:.2} s", self.params.sources.len(), seconds))
    }

    pub fn project_json(&self) -> String {
        let mut v = self.params.to_json();
        let model = self.active_model.and_then(|i| self.models.get(i));
        v["model"] = json!(model.map(|m| m.path.clone()));
        v["parts"] = json!(model.map(|m| m.parts.iter().map(|p| p.enabled).collect::<Vec<_>>()));
        serde_json::to_string_pretty(&v).unwrap_or_default()
    }

    pub fn project_load(&mut self, text: &str) -> Result<(), String> {
        let v: serde_json::Value = serde_json::from_str(text).map_err(|e| e.to_string())?;
        self.params = Params::from_json(&v)?;
        self.invalidate();
        self.mesh = None;
        self.active_model = None;
        if let Some(path) = v.get("model").and_then(|m| m.as_str()) {
            let h = self.load_model(path)?;
            if let Some(flags) = v.get("parts").and_then(|p| p.as_array()) {
                for (i, f) in flags.iter().enumerate() {
                    self.set_part(h, i, f.as_bool().unwrap_or(true));
                }
            }
            self.use_model(Some(h));
        }
        Ok(())
    }
}

/// Laplacian smoothing of a per-vertex scalar over the mesh edges.
fn smooth_scalar(mesh: &Mesh, s: &mut [f32], passes: usize) {
    let (start, nbr) = mesher::adjacency(mesh.vert_count(), &mesh.idx);
    for _ in 0..passes {
        let src = s.to_vec();
        for (v, out) in s.iter_mut().enumerate() {
            let ring = &nbr[start[v]..start[v + 1]];
            if !ring.is_empty() {
                let avg = ring.iter().map(|&u| src[u as usize]).sum::<f32>() / ring.len() as f32;
                *out = 0.5 * src[v] + 0.5 * avg;
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn end_to_end_wav_round_trip() {
        let mut kit = BodyKit::new();
        kit.param_set("sr", 11025.0);
        kit.param_set("steps", 240.0);
        kit.param_set("margin", 10.0);
        kit.param_set("sponge", 6.0);
        kit.source_add([0.0, 1.25, 0.0], 0, 500.0);
        kit.source_add([0.5, 1.4, 0.0], 1, 900.0);
        kit.sim_begin().unwrap();
        while !kit.sim_done() {
            kit.sim_advance(1000.0);
        }
        kit.mesh_from_sim().unwrap();
        let (_, tris, wt) = kit.mesh_info();
        assert!(tris > 1000 && wt);

        let path = std::env::temp_dir().join("ling_body_kit_test.wav");
        let path = path.to_str().unwrap();
        kit.wav_export(path).unwrap();
        let wav = codec::decode_body(&std::fs::read(path).unwrap()).unwrap();
        let repeat = kit.params.get("wav_repeat") as usize;
        let period = 240 + (kit.params.get("wav_gap") * 11025.0).round() as usize;
        assert_eq!(wav.left.len(), repeat * period);
        assert!(!kit.mesh_push().is_empty(), "a simulated mesh carries per-vertex skin displacement");

        let mut other = BodyKit::new();
        let msg = other.wav_import(path, false).unwrap();
        assert!(msg.starts_with("exact recipe"), "{msg}");
        assert_eq!(other.mesh_info().1, tris);
        assert_eq!(other.params.sources.len(), 2);

        let mut rough = BodyKit::new();
        let msg = rough.wav_import(path, true).unwrap();
        assert!(msg.starts_with("estimated"), "{msg}");
        assert!(!rough.params.sources.is_empty());
        let _ = std::fs::remove_file(path);
    }

    #[test]
    fn creases_detail_and_listening_work_on_a_simulated_body() {
        let mut kit = BodyKit::new();
        kit.param_set("sr", 11025.0);
        kit.param_set("steps", 300.0);
        kit.param_set("margin", 10.0);
        kit.param_set("sponge", 6.0);
        kit.param_set("sig_dur", 0.1);
        kit.param_set("sig_sr", 12000.0);
        kit.source_add([0.0, 1.25, 0.0], 0, 500.0);
        kit.source_add([0.4, 1.4, 0.0], 1, 800.0);
        assert!(kit.crease_detect().is_err(), "creases need a simulation");
        kit.sim_begin().unwrap();
        while !kit.sim_done() {
            kit.sim_advance(1000.0);
        }
        kit.mesh_from_sim().unwrap();
        let smooth_tris = kit.mesh_info().1;

        let msg = kit.crease_detect().unwrap();
        assert!(!kit.creases().is_empty(), "{msg}");
        kit.crease_apply().unwrap();
        let (_, tris, watertight) = kit.mesh_info();
        assert!(tris >= smooth_tris * 4 && watertight, "{tris} triangles, watertight {watertight}");
        kit.detail_reset().unwrap();
        assert_eq!(kit.mesh_info().1, smooth_tris);

        let dir = std::env::temp_dir();
        let json = dir.join("ling_body_creases.json");
        let py = dir.join("ling_body_creases.py");
        kit.crease_export(json.to_str().unwrap()).unwrap();
        kit.crease_export(py.to_str().unwrap()).unwrap();
        assert!(std::fs::read_to_string(&json).unwrap().contains("ling-body/creases"));
        assert!(std::fs::read_to_string(&py).unwrap().contains("bpy.data.curves.new"));
        assert!(kit.crease_export("x.txt").is_err());

        let wav = dir.join("ling_body_listen.wav");
        let total = kit.listen_begin().unwrap();
        assert_eq!(total, 1200);
        while !kit.listen_done() {
            kit.listen_advance(1000.0);
        }
        kit.listen_finish(wav.to_str().unwrap()).unwrap();
        assert_eq!(kit.listen_spectrum().len(), 48);
        assert!(!kit.notes().is_empty());
        let heard = codec::decode_body(&std::fs::read(&wav).unwrap()).unwrap();
        assert!(heard.left.iter().any(|v| v.abs() > 0.1));
        for f in [json, py, wav] {
            let _ = std::fs::remove_file(f);
        }
    }
}

← Back to index