ling-lang
↓ 0 downloadsLing - The Omniglot Systems Language
Keywords systems, polyglot, ai, crypto, game
lingfu install ling-langCode / Files (995)
crates/ling-daw/src/api/rvc_api.rs
//! Voice skins (`daw_rvc_*`): a vocal is converted to another voice with an RVC model. One job at a time runs in the background, by
//! one of two engines:
//! * native (`script` = "native", builds with the `llm` feature): `ling_ai::rvc` on candle (CUDA or CPU) in a thread - HuBERT, the RVC
//! synthesizer and NSF-HiFiGAN in Rust, the pitch from the DAW's own tracker (`vocal::track_pitch`); no Python;
//! * an RVC WebUI install on this machine (its own Python, HuBERT, RMVPE; LingStationX's `tools/rvc_skin.py` drives it).
//!
//! * an audio clip: its recording is converted and the clip plays the result (the original is kept: `rvc.orig.<result path>` in the
//! project meta, so Restore puts it back);
//! * a MIDI clip (a Ling Voice part singing its lyrics, or any instrument): the track is rendered alone over the clip, converted, and the
//! result lands on a new audio track under it; the source track is muted.
//!
//! Models: `.pth` files with an optional `.index` beside them (`Name.pth` + `Name*.index`, or a folder `Name/` holding both), or an RVC
//! install's layout (`weights/Name.pth` + `logs/Name/added_*.index`).
use super::{Daw, A, Val};
use crate::model::*;
use std::path::{Path, PathBuf};
use std::process::Child;
use std::sync::{Arc, Mutex, OnceLock};
enum Target {
Skin { t: usize, c: usize, orig: String },
NewTrack { t: usize, start: f64, name: String },
}
enum Run {
/// rendering the part alone (a copy of the project, off the interface thread); it starts the conversion when done and hands
/// back the next stage; progress 0..1000, a cancel switch
Prep(Option<std::thread::JoinHandle<Result<Run, String>>>, Arc<std::sync::atomic::AtomicU32>, Arc<std::sync::atomic::AtomicBool>),
Proc(Child),
/// the native conversion: its thread and its progress 0..100
Native(Option<std::thread::JoinHandle<Result<(), String>>>, Arc<std::sync::atomic::AtomicU32>),
}
struct Job {
run: Run,
target: Target,
inp: PathBuf,
out: PathBuf,
log: PathBuf,
model: String,
started: std::time::Instant,
}
/// everything needed to start a conversion (now, or after the background render of the part)
struct Launch {
native: bool,
py: PathBuf,
rvc: PathBuf,
script: PathBuf,
model: PathBuf,
index: String,
semis: f64,
method: String,
index_rate: f64,
protect: f64,
mix: f64,
cpu: bool,
inp: PathBuf,
out: PathBuf,
log: PathBuf,
id: u64,
}
impl Launch {
fn start(&self) -> Result<Run, String> {
let abs = |p: PathBuf| if p.is_absolute() { p } else { std::env::current_dir().map(|d| d.join(&p)).unwrap_or(p) };
if self.native {
#[cfg(feature = "llm")]
{
let hub = abs(hubert_for(&self.rvc).unwrap_or_default());
let prog = Arc::new(std::sync::atomic::AtomicU32::new(0));
let opts = ling_ai::rvc::Options { transpose: self.semis, rms_mix: self.mix.clamp(0.0, 1.0) as f32, speaker: 0, seed: self.id };
let device = if self.cpu { -1 } else { 0 };
let (p2, i2, o2, m2) = (prog.clone(), self.inp.clone(), self.out.clone(), self.model.clone());
let h = std::thread::spawn(move || native_convert(&hub, &m2, &i2, &o2, &opts, device, &p2));
return Ok(Run::Native(Some(h), prog));
}
#[cfg(not(feature = "llm"))]
return Err("this ling.exe has no native engine".into());
}
let logf = std::fs::File::create(&self.log).map_err(|e| e.to_string())?;
let mut cmd = std::process::Command::new(&self.py);
cmd.arg(&self.script)
.arg(&self.inp)
.arg(&self.out)
.arg(&self.model)
.arg(if self.index.is_empty() { "-".to_string() } else { self.index.clone() })
.arg(format!("{}", self.semis))
.arg(if self.method.is_empty() { "rmvpe".to_string() } else { self.method.clone() })
.arg(format!("{}", self.index_rate.clamp(0.0, 1.0)))
.arg(format!("{}", self.protect.clamp(0.0, 0.5)))
.arg(format!("{}", self.mix.clamp(0.0, 1.0)))
.arg(if self.cpu { "1" } else { "0" })
.current_dir(&self.rvc)
.env("PYTHONUNBUFFERED", "1")
.stdout(logf.try_clone().map_err(|e| e.to_string())?)
.stderr(logf);
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
cmd.creation_flags(0x0800_0000); // no console window
}
let child = cmd.spawn().map_err(|e| format!("could not start {}: {e}", self.py.display()))?;
Ok(Run::Proc(child))
}
}
fn job() -> &'static Mutex<Option<Job>> {
static J: OnceLock<Mutex<Option<Job>>> = OnceLock::new();
J.get_or_init(|| Mutex::new(None))
}
/// the Python of an RVC install
pub fn rvc_python(dir: &Path) -> Option<PathBuf> {
[".venv/Scripts/python.exe", "venv/Scripts/python.exe", "runtime/python.exe", ".venv/bin/python", "venv/bin/python"].iter().map(|p| dir.join(p)).find(|p| p.is_file())
}
fn stem(p: &Path) -> String {
p.file_stem().map(|s| s.to_string_lossy().into_owned()).unwrap_or_default()
}
/// the index file that belongs with a model: next to it, in a folder of its name, or in an install's logs/<name>/ ("added_" first)
fn index_for(pth: &Path) -> Option<PathBuf> {
let name = stem(pth).to_lowercase();
let mut dirs: Vec<PathBuf> = vec![];
if let Some(d) = pth.parent() {
dirs.push(d.to_path_buf());
dirs.push(d.join(stem(pth)));
if let Some(root) = d.parent() {
dirs.push(root.join("logs").join(stem(pth)));
}
}
let mut found: Vec<PathBuf> = vec![];
for d in dirs {
let Ok(rd) = std::fs::read_dir(&d) else { continue };
for e in rd.flatten() {
let p = e.path();
let f = p.file_name().map(|s| s.to_string_lossy().to_lowercase()).unwrap_or_default();
// beside the model the index must carry the model's name; in its own folder any index is its index
let own_folder = d.file_name().map(|s| s.to_string_lossy().to_lowercase() == name).unwrap_or(false);
if f.ends_with(".index") && (own_folder || f.contains(&name)) {
found.push(p);
}
}
if !found.is_empty() {
break;
}
}
found.sort_by_key(|p| !p.file_name().map(|s| s.to_string_lossy().contains("added")).unwrap_or(false));
found.into_iter().next()
}
/// every model in `dirs`: (name, model path, index path or "")
fn scan(dirs: &[PathBuf]) -> Vec<(String, String, String)> {
let mut out: Vec<(String, String, String)> = vec![];
let add = |p: PathBuf, out: &mut Vec<(String, String, String)>| {
let n = stem(&p);
// training checkpoints (G_/D_) are not voices
if n.starts_with("G_") || n.starts_with("D_") || out.iter().any(|x| x.0.eq_ignore_ascii_case(&n)) {
return;
}
let idx = index_for(&p).map(|i| i.to_string_lossy().into_owned()).unwrap_or_default();
out.push((n, p.to_string_lossy().into_owned(), idx));
};
for d in dirs {
let Ok(rd) = std::fs::read_dir(d) else { continue };
let mut entries: Vec<PathBuf> = rd.flatten().map(|e| e.path()).collect();
entries.sort();
for p in entries {
if p.is_file() && p.extension().map(|e| e.eq_ignore_ascii_case("pth")).unwrap_or(false) {
add(p, &mut out);
} else if p.is_dir() {
// a folder per voice
if let Ok(sub) = std::fs::read_dir(&p) {
for s in sub.flatten() {
let sp = s.path();
if sp.extension().map(|e| e.eq_ignore_ascii_case("pth")).unwrap_or(false) {
add(sp, &mut out);
}
}
}
}
}
}
out
}
/// the loaded native converter, kept for the next job with the same files
#[cfg(feature = "llm")]
fn native_cache() -> &'static Mutex<Option<(PathBuf, PathBuf, Arc<ling_ai::rvc::Rvc>)>> {
static C: OnceLock<Mutex<Option<(PathBuf, PathBuf, Arc<ling_ai::rvc::Rvc>)>>> = OnceLock::new();
C.get_or_init(|| Mutex::new(None))
}
/// the HuBERT model the native engine reads: in the RVC folder, or in models/rvc
fn hubert_for(rvc: &Path) -> Option<PathBuf> {
[rvc.join("hubert_base.pt"), PathBuf::from("models/rvc/hubert_base.pt")].into_iter().find(|p| p.is_file())
}
#[cfg(feature = "llm")]
fn native_convert(hub: &Path, model: &Path, inp: &Path, out: &Path, opts: &ling_ai::rvc::Options, device: i64, prog: &std::sync::atomic::AtomicU32) -> Result<(), String> {
use std::sync::atomic::Ordering;
prog.store(2, Ordering::Relaxed);
let rvc = {
let mut c = native_cache().lock().unwrap_or_else(|p| p.into_inner());
match c.as_ref() {
Some((h, m, r)) if h == hub && m == model => r.clone(),
_ => {
let r = Arc::new(ling_ai::rvc::Rvc::load(hub, model, device)?);
*c = Some((hub.to_path_buf(), model.to_path_buf(), r.clone()));
r
}
}
};
prog.store(10, Ordering::Relaxed);
let s = load_sample(&inp.to_string_lossy())?;
let mono: Vec<f32> = s.stereo.chunks_exact(2).map(|f| (f[0] + f[1]) * 0.5).collect();
let a16 = ling_ai::whisper::resample_to_16k(&mono, s.rate);
// the pitch every 10 ms (the tracker gives every 5 ms)
let p = crate::vocal::track_pitch(&a16, 16000.0);
let step = (0.01 / p.hop).round().max(1.0) as usize;
let f0: Vec<f32> = (0..a16.len() / 160).map(|j| p.f0.get(j * step).copied().unwrap_or(0.0)).collect();
prog.store(20, Ordering::Relaxed);
let y = rvc.convert(&a16, &f0, opts, &mut |f| prog.store((20.0 + f * 75.0) as u32, Ordering::Relaxed))?;
let stereo: Vec<f32> = y.iter().flat_map(|x| [*x, *x]).collect();
write_wav(out, &stereo, rvc.voice.sr)?;
prog.store(100, Ordering::Relaxed);
Ok(())
}
fn write_wav(path: &Path, stereo: &[f32], rate: u32) -> Result<(), String> {
let spec = hound::WavSpec { channels: 2, sample_rate: rate, bits_per_sample: 16, sample_format: hound::SampleFormat::Int };
let mut w = hound::WavWriter::create(path, spec).map_err(|e| e.to_string())?;
for s in stereo {
w.write_sample((s.clamp(-1.0, 1.0) * 32767.0) as i16).map_err(|e| e.to_string())?;
}
w.finalize().map_err(|e| e.to_string())
}
/// the last lines of the job log (the reason a conversion failed)
fn log_tail(p: &Path, n: usize) -> String {
let t = std::fs::read_to_string(p).unwrap_or_default();
let lines: Vec<&str> = t.lines().filter(|l| !l.trim().is_empty() && !l.contains("| INFO |") && !l.contains("FutureWarning")).collect();
lines[lines.len().saturating_sub(n)..].join(" / ")
}
impl Daw {
fn rvc_dir(&self) -> PathBuf {
if self.path.is_empty() {
PathBuf::from("cache").join("rvc")
} else {
Path::new(&self.path).parent().map(|p| p.join("audio").join("rvc")).unwrap_or_else(|| PathBuf::from("cache").join("rvc"))
}
}
/// apply a finished job: the clip plays the converted voice, or a new track carries it
fn rvc_apply(&mut self, j: Job) -> Result<String, String> {
let s = load_sample(&j.out.to_string_lossy())?;
let _ = self.dispatch("daw_undo_push", &A(&[]));
let eng = self.out.engine.clone();
let mut en = eng.lock().unwrap_or_else(|p| p.into_inner());
let bpm = en.project.bpm.max(20.0);
let path = s.path.clone();
match j.target {
Target::Skin { t, c, orig } => {
let Some(clip) = en.project.tracks.get_mut(t).and_then(|x| x.clips.get_mut(c)) else { return Err("the clip is gone".into()) };
let ClipData::Audio { sample, .. } = &mut clip.data else { return Err("the clip is no longer audio".into()) };
let named = Arc::new(Sample::new(format!("{} ({})", sample.name.split(" (").next().unwrap_or(&sample.name), j.model), path.clone(), s.rate, s.stereo.clone()));
*sample = named.clone();
let key = format!("rvc.orig.{path}");
match en.project.meta.iter_mut().find(|(k, _)| *k == key) {
Some(e) => e.1 = orig,
None => en.project.meta.push((key, orig)),
}
drop(en);
self.samples.insert(path, named);
self.touch();
Ok(format!("Voice skin: the clip now sings with {} ({:.0} s)", j.model, j.started.elapsed().as_secs_f64()))
}
Target::NewTrack { t, start, name } => {
let n = en.project.tracks.len();
if n >= MAX_TRACKS {
return Err("no room for another track".into());
}
let col = en.project.tracks.get(t).map(|x| x.color).unwrap_or(0x9A53D6);
let mut tr = Track::new(&format!("{name} ({})", j.model), TrackKind::Audio, col, None);
let len = (s.seconds() * bpm / 60.0).max(0.25);
let named = Arc::new(Sample::new(format!("{name} ({})", j.model), path.clone(), s.rate, s.stereo.clone()));
tr.clips.push(Clip { start, len, name: format!("{name} ({})", j.model), loop_len: 0.0, lyric_text: String::new(), lyric_lang: String::new(), tag: String::new(), data: ClipData::Audio { sample: named.clone(), offset_s: 0.0, gain: 1.0, pitch: 0.0, warp: 0.0 } });
tr.volume = en.project.tracks.get(t).map(|x| x.volume).unwrap_or(0.9);
en.project.tracks.push(tr);
if let Some(src) = en.project.tracks.get_mut(t) {
src.mute = true;
}
drop(en);
self.samples.insert(path, named);
self.touch();
Ok(format!("Voice skin: {name} sung by {} on a new track (the part is muted) ({:.0} s)", j.model, j.started.elapsed().as_secs_f64()))
}
}
}
pub(super) fn rvc_dispatch(&mut self, name: &str, a: &A) -> Result<Val, String> {
let eng = self.out.engine.clone();
macro_rules! ok {
($v:expr) => {
return Ok(Val::from($v))
};
}
match name {
"daw_rvc_info" => {
// (RVC folder) -> [python ("" = not an RVC install), HuBERT 0/1, RMVPE 0/1]
let d = PathBuf::from(a.s(0));
let py = rvc_python(&d).map(|p| p.to_string_lossy().into_owned()).unwrap_or_default();
let ok_pipe = d.join("vc_infer_pipeline.py").is_file() || d.join("infer").is_dir();
// [3]: the native engine can run (this build has it and a HuBERT is there)
let native = cfg!(feature = "llm") && hubert_for(&d).is_some();
ok!(Val::List(vec![Val::Str(if ok_pipe { py } else { String::new() }), Val::Num(d.join("hubert_base.pt").is_file() as u8 as f64), Val::Num(d.join("rmvpe.pt").is_file() as u8 as f64), Val::Num(native as u8 as f64)]))
}
"daw_rvc_models" => {
// (folders separated by ';') -> [name, model path, index path or ""] * N
let dirs: Vec<PathBuf> = a.s(0).split(';').filter(|s| !s.trim().is_empty()).map(|s| PathBuf::from(s.trim())).collect();
let mut v = vec![];
for (n, p, i) in scan(&dirs) {
v.extend([Val::Str(n), Val::Str(p), Val::Str(i)]);
}
ok!(Val::List(v))
}
"daw_rvc_start" => {
// (track, clip (-1 = the whole track), RVC folder, script, model, index, semitones, method, index rate, protect, volume mix, cpu 0/1)
// -> true when started
let (t, c) = (a.u(0), a.u(1));
let rvc = PathBuf::from(a.s(2));
if job().lock().unwrap_or_else(|p| p.into_inner()).is_some() {
self.status = "Voice skin: a conversion is already running".into();
ok!(false)
}
// script "native": the Rust engine (no Python); otherwise the RVC install's Python runs tools/rvc_skin.py
let native = a.s(3) == "native";
let py = if native {
PathBuf::new()
} else {
let Some(py) = rvc_python(&rvc) else {
self.status = format!("Voice skin: no RVC install (with its Python) in {}", rvc.display());
ok!(false)
};
py
};
if native && (!cfg!(feature = "llm") || hubert_for(&rvc).is_none()) {
self.status = if cfg!(feature = "llm") { "Voice skin: hubert_base.pt not found (put it in models/rvc or the RVC folder)".into() } else { "Voice skin: this ling.exe has no native engine (build with --features llm)".into() };
ok!(false)
}
let model = PathBuf::from(a.s(4));
if !model.is_file() {
self.status = format!("Voice skin: model not found ({})", model.display());
ok!(false)
}
let id = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_millis() % 100_000_000).unwrap_or(0);
// RVC runs in its own folder: every path it gets is absolute
let abs = |p: PathBuf| if p.is_absolute() { p } else { std::env::current_dir().map(|d| d.join(&p)).unwrap_or(p) };
let work = abs(self.rvc_dir());
std::fs::create_dir_all(&work).map_err(|e| e.to_string())?;
let model = abs(model);
let model_name = stem(&model);
let inp = work.join(format!("rvc_{id}_in.wav"));
let out = work.join(format!("rvc_{id}_{}.wav", model_name.replace(' ', "_")));
// what is converted: an audio clip's recording, or the track rendered alone over a MIDI clip (that render runs in the
// background job, from a copy of the project, so neither the interface nor the audio waits for it)
let mut prep: Option<(crate::project::ProjectDoc, f32, f64, f64)> = None;
let target = {
let mut en = eng.lock().unwrap_or_else(|p| p.into_inner());
let Some(tr) = en.project.tracks.get(t) else { ok!(false) };
// clip -1: the whole track (every MIDI clip on it), rendered alone and sung as one take
let whole = a.n(1) < 0.0;
let span = if whole && !tr.clips.is_empty() {
Some((tr.clips.iter().map(|k| k.start).fold(f64::MAX, f64::min), tr.clips.iter().map(|k| k.end()).fold(0.0, f64::max)))
} else {
None
};
let Some(clip) = tr.clips.get(if whole { 0 } else { c }) else { ok!(false) };
match &clip.data {
_ if span.is_some() => {
let (start, end) = span.unwrap();
let tname = tr.name.clone();
prep = Some((crate::project::to_doc(&en), en.sr, start, end));
drop(en);
Target::NewTrack { t, start, name: tname }
}
ClipData::Audio { sample, .. } => {
// a clip skinned before is converted from its original recording again
let cur = sample.clone();
let key = format!("rvc.orig.{}", cur.path);
let orig = en.project.meta.iter().find(|(k, _)| *k == key).map(|(_, v)| v.clone()).unwrap_or_else(|| cur.path.clone());
drop(en);
if orig != cur.path {
let o = load_sample(&orig)?;
write_wav(&inp, &o.stereo, o.rate)?;
} else {
write_wav(&inp, &cur.stereo, cur.rate)?;
}
Target::Skin { t, c, orig }
}
ClipData::Midi(_) => {
let (start, end, tname) = (clip.start, clip.end(), tr.name.clone());
prep = Some((crate::project::to_doc(&en), en.sr, start, end));
drop(en);
Target::NewTrack { t, start, name: tname }
}
}
};
let log = work.join(format!("rvc_{id}.log"));
let launch = Launch {
native,
py: py.clone(),
rvc: rvc.clone(),
script: abs(PathBuf::from(a.s(3))),
model: model.clone(),
index: a.s(5),
semis: a.n(6).round(),
method: a.s(7),
index_rate: a.n(8),
protect: a.n(9),
mix: a.n(10),
cpu: a.n(11) > 0.5,
inp: inp.clone(),
out: out.clone(),
log: log.clone(),
id: id as u64,
};
if let Some((doc, sr, start, end)) = prep {
let solo = match &target {
Target::NewTrack { t, .. } => *t,
Target::Skin { t, .. } => *t,
};
let base = Path::new(&self.path).parent().map(|p| p.to_path_buf());
let mut samples = self.samples.clone();
let prog = Arc::new(std::sync::atomic::AtomicU32::new(0));
let cancel = Arc::new(std::sync::atomic::AtomicBool::new(false));
let (p2, c2) = (prog.clone(), cancel.clone());
let h = std::thread::spawn(move || -> Result<Run, String> {
let mut eng = crate::engine::Engine::new(sr);
crate::project::apply_doc(&mut eng, &doc, base.as_deref(), &mut samples);
for (i, x) in eng.project.tracks.iter_mut().enumerate() {
x.solo = i == solo;
if i == solo {
x.mute = false;
}
}
let Some(data) = eng.bounce_range_progress(start, end, 0.6, &p2, &c2, 0, 1000) else { return Err("cancelled".into()) };
write_wav(&launch.inp, &data, sr as u32)?;
if c2.load(std::sync::atomic::Ordering::Relaxed) {
return Err("cancelled".into());
}
launch.start()
});
*job().lock().unwrap_or_else(|p| p.into_inner()) = Some(Job { run: Run::Prep(Some(h), prog, cancel), target, inp, out, log, model: model_name.clone(), started: std::time::Instant::now() });
self.status = format!("Voice skin: rendering the part, then {model_name} sings it ...");
ok!(true)
}
let run = launch.start()?;
*job().lock().unwrap_or_else(|p| p.into_inner()) = Some(Job { run, target, inp, out, log, model: model_name.clone(), started: std::time::Instant::now() });
self.status = format!("Voice skin: converting with {model_name} ...");
ok!(true)
}
"daw_rvc_poll" => {
// () -> [state (0 nothing, 1 running, 2 just finished and applied, -1 failed), message, seconds, progress 0..100]
let mut g = job().lock().unwrap_or_else(|p| p.into_inner());
let Some(j) = g.as_mut() else { ok!(Val::List(vec![Val::Num(0.0), Val::Str(String::new()), Val::Num(0.0), Val::Num(0.0)])) };
let secs = j.started.elapsed().as_secs_f64();
// where the job is: (still running? progress, what it does) and, when it ended, success or the reason
// the prep stage: rendering the part; when it is done its thread has started the conversion - the job moves on
if let Run::Prep(h, prog, _) = &mut j.run {
if h.as_ref().map_or(false, |h| !h.is_finished()) {
let pct = prog.load(std::sync::atomic::Ordering::Relaxed) as f64 / 1000.0 * 15.0;
ok!(Val::List(vec![Val::Num(1.0), Val::Str(format!("rendering the part alone ({:.0} %)", pct / 15.0 * 100.0)), Val::Num(secs), Val::Num(pct)]))
}
let r = h.take().map_or(Err("the render stopped".into()), |h| h.join().unwrap_or_else(|_| Err("the render crashed".into())));
match r {
Ok(next) => {
j.run = next;
ok!(Val::List(vec![Val::Num(1.0), Val::Str("starting the voice model".into()), Val::Num(secs), Val::Num(15.0)]))
}
Err(e) => {
let j = g.take().unwrap();
drop(g);
let _ = std::fs::remove_file(&j.inp);
let m = format!("Voice skin failed: {e}");
self.status = m.clone();
ok!(Val::List(vec![Val::Num(-1.0), Val::Str(m), Val::Num(secs), Val::Num(0.0)]))
}
}
}
let (done, pct, what): (Option<Result<(), String>>, f64, String) = match &mut j.run {
Run::Proc(child) => {
let log = std::fs::read_to_string(&j.log).unwrap_or_default();
let prog = log.lines().rev().find_map(|l| l.strip_prefix("PROGRESS ")).unwrap_or("0 starting").to_string();
let (pct, what) = prog.split_once(' ').map(|(a, b)| (a.to_string(), b.to_string())).unwrap_or((prog.clone(), String::new()));
let done = match child.try_wait() {
Ok(None) => None,
Ok(Some(st)) => Some(if st.success() { Ok(()) } else { Err(log_tail(&j.log, 3)) }),
Err(e) => Some(Err(e.to_string())),
};
(done, pct.parse().unwrap_or(0.0), what)
}
Run::Prep(..) => (None, 0.0, String::new()),
Run::Native(h, prog) => {
let pct = prog.load(std::sync::atomic::Ordering::Relaxed) as f64;
(if h.as_ref().map_or(true, |h| h.is_finished()) { Some(Ok(())) } else { None }, pct, if pct < 10.0 { "loading the voice model".into() } else if pct < 20.0 { "tracking the pitch".into() } else { "converting (native)".into() })
}
};
match done {
None => ok!(Val::List(vec![Val::Num(1.0), Val::Str(what), Val::Num(secs), Val::Num(pct)])),
Some(res) => {
let mut j = g.take().unwrap();
drop(g);
// a native thread's own answer
let res = match &mut j.run {
Run::Native(h, _) => h.take().map_or(Ok(()), |h| h.join().unwrap_or_else(|_| Err("the conversion crashed".into()))),
Run::Proc(_) | Run::Prep(..) => res,
};
let reason = res.as_ref().err().cloned().unwrap_or_default();
// the copy of the vocal that was sent is not needed any more
let _ = std::fs::remove_file(&j.inp);
if res.is_ok() && j.out.is_file() {
let _ = std::fs::remove_file(&j.log);
match self.rvc_apply(j) {
Ok(m) => {
self.status = m.clone();
ok!(Val::List(vec![Val::Num(2.0), Val::Str(m), Val::Num(secs), Val::Num(100.0)]))
}
Err(e) => {
self.status = format!("Voice skin failed: {e}");
ok!(Val::List(vec![Val::Num(-1.0), Val::Str(self.status.clone()), Val::Num(secs), Val::Num(0.0)]))
}
}
}
let m = format!("Voice skin failed: {}", if reason.is_empty() { log_tail(&j.log, 3) } else { reason });
self.status = m.clone();
ok!(Val::List(vec![Val::Num(-1.0), Val::Str(m), Val::Num(secs), Val::Num(0.0)]))
}
}
}
"daw_rvc_cancel" => {
if let Some(j) = job().lock().unwrap_or_else(|p| p.into_inner()).take() {
// a process is stopped; a native thread finishes on its own and its result is dropped
match j.run {
Run::Proc(mut child) => {
let _ = child.kill();
let _ = child.wait();
}
// the render stops at its next block and starts nothing
Run::Prep(_, _, cancel) => cancel.store(true, std::sync::atomic::Ordering::Relaxed),
Run::Native(..) => {}
}
for p in [&j.inp, &j.out, &j.log] {
let _ = std::fs::remove_file(p);
}
self.status = "Voice skin: cancelled".into();
}
ok!(true)
}
"daw_rvc_original" => {
// (track, clip) -> the recording a skinned clip came from, "" when the clip is not skinned
let en = eng.lock().unwrap_or_else(|p| p.into_inner());
let Some(Clip { data: ClipData::Audio { sample, .. }, .. }) = en.project.tracks.get(a.u(0)).and_then(|t| t.clips.get(a.u(1))) else { ok!("") };
let key = format!("rvc.orig.{}", sample.path);
ok!(en.project.meta.iter().find(|(k, _)| *k == key).map(|(_, v)| v.clone()).unwrap_or_default())
}
"daw_rvc_restore" => {
// (track, clip) -> true: a skinned clip plays its original recording again
let orig = {
let en = eng.lock().unwrap_or_else(|p| p.into_inner());
let Some(Clip { data: ClipData::Audio { sample, .. }, .. }) = en.project.tracks.get(a.u(0)).and_then(|t| t.clips.get(a.u(1))) else { ok!(false) };
let key = format!("rvc.orig.{}", sample.path);
en.project.meta.iter().find(|(k, _)| *k == key).map(|(_, v)| v.clone())
};
let Some(orig) = orig else { ok!(false) };
let s = self.load_sample_cached(&orig)?;
let mut en = eng.lock().unwrap_or_else(|p| p.into_inner());
if let Some(Clip { data: ClipData::Audio { sample, .. }, .. }) = en.project.tracks.get_mut(a.u(0)).and_then(|t| t.clips.get_mut(a.u(1))) {
*sample = s;
}
drop(en);
self.touch();
ok!(true)
}
_ => Err(format!("unknown builtin {name}")),
}
}
}