灵符lingfu
L

ling-daw

↓ 0 downloads

Ling DAW engine: synth, physical-modeling drums, effects, mixer, sequencer, project I/O

lingfu install ling-daw
LicenseApache-2.0 OR MIT
AuthorsLing Lin <taellinglin@gmail.com>, Sanny Lin <SannyLing53@gmail.com>
READMECode / FilesVersionsDependency GraphDependentsSecurityBuildsSettings

Code / Files (91)

src/api/sheet_api.rs
//! Builtins for the sheet-music export (`daw_sheet_*`, see `sheet.rs`).
use super::{Daw, A, Val};
use crate::sheet::{self, Opts};

impl Daw {
    pub(super) fn sheet_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_sheet_scan" => {
                // -> [bars, then (track, name, notes, drums 0/1, has lyrics 0/1)*] for every instrument track that has notes
                let en = eng.lock().unwrap_or_else(|p| p.into_inner());
                let p = &en.project;
                let mut v = vec![Val::Num((p.end_beat().max(4.0) / 4.0).ceil())];
                let lyr: Vec<usize> = crate::karaoke::lyric_tracks(p).into_iter().map(|x| x.0).collect();
                for (i, t) in p.tracks.iter().enumerate() {
                    if !matches!(t.kind, crate::model::TrackKind::Instrument) {
                        continue;
                    }
                    let n: usize = t.clips.iter().map(|c| if let crate::model::ClipData::Midi(m) = &c.data { m.len() } else { 0 }).sum();
                    if n == 0 {
                        continue;
                    }
                    v.extend([Val::Num(i as f64), Val::Str(t.name.clone()), Val::Num(n as f64), Val::Num((t.inst_kind == "drums") as u8 as f64), Val::Num(lyr.contains(&i) as u8 as f64)]);
                }
                ok!(Val::List(v))
            }
            "daw_sheet_write" => {
                // (folder, base name, options "k=v;...", fonts folder) -> the files written; [] when there is nothing to draw
                let (dir, base, opts, fdir) = (a.s(0), a.s(1), a.s(2), a.s(3));
                let o = Opts::parse(&opts);
                let sheet = {
                    let en = eng.lock().unwrap_or_else(|p| p.into_inner());
                    let p = &en.project;
                    sheet::build(p, &o, &|t| crate::karaoke::track_syllables(p, t))
                };
                if sheet.tracks == 0 {
                    self.status = "Sheet music: no tracks with notes in that range".into();
                    return ok!(Val::List(vec![]));
                }
                let dirp = std::path::Path::new(&dir);
                std::fs::create_dir_all(dirp).map_err(|e| format!("{dir}: {e}"))?;
                let mut fonts: Vec<(&str, Vec<u8>, &str)> = vec![];
                if let Ok(d) = std::fs::read(std::path::Path::new(&fdir).join("Daemon.otf")) {
                    fonts.push(("LSXDaemon", d, "font/otf"));
                }
                if let Ok(d) = std::fs::read(std::path::Path::new(&fdir).join("Chesilin.ttf")) {
                    fonts.push(("LSXChesilin", d, "font/ttf"));
                }
                let css = sheet::font_css(&fonts);
                let mut out: Vec<Val> = vec![];
                let title = if o.title.is_empty() { base.clone() } else { o.title.clone() };
                let n = sheet.pages.len();
                for (i, pg) in sheet.pages.iter().enumerate() {
                    let file = if n == 1 { format!("{base}.svg") } else { format!("{base}_p{}.svg", i + 1) };
                    std::fs::write(dirp.join(&file), sheet::svg_page(pg, o.landscape, &css)).map_err(|e| format!("{file}: {e}"))?;
                    out.push(Val::Str(file));
                }
                let file = format!("{base}.html");
                std::fs::write(dirp.join(&file), sheet::html(&title, &sheet.pages, o.landscape, &css)).map_err(|e| format!("{file}: {e}"))?;
                out.push(Val::Str(file.clone()));
                let mode = a.s(4);
                let mut pdf_err = String::new();
                if mode.contains("xml") {
                    // the same notes as a MusicXML score for MuseScore, Sibelius, Dorico ...
                    let (xml, n) = {
                        let en = eng.lock().unwrap_or_else(|p| p.into_inner());
                        let p = &en.project;
                        crate::musicxml::musicxml(p, &crate::musicxml::MxOpts { tracks: o.tracks.clone(), from_bar: o.from_bar, to_bar: o.to_bar, title: o.title.clone() }, &|t| crate::karaoke::track_syllables(p, t))
                    };
                    if n > 0 {
                        let name = format!("{base}.musicxml");
                        std::fs::write(dirp.join(&name), xml).map_err(|e| format!("{name}: {e}"))?;
                        out.push(Val::Str(name));
                    }
                }
                if mode.contains("pdf") || mode.contains("parts") {
                    // the whole score as one PDF, and / or one PDF per part (each part is its own score of a single track)
                    if mode.contains("pdf") {
                        let name = format!("{base}.pdf");
                        match html_to_pdf(&dirp.join(&file), &dirp.join(&name)) {
                            Ok(()) => out.push(Val::Str(name)),
                            Err(e) => pdf_err = e,
                        }
                    }
                    if mode.contains("parts") && pdf_err.is_empty() {
                        let ids: Vec<usize> = if o.tracks.is_empty() {
                            let en = eng.lock().unwrap_or_else(|p| p.into_inner());
                            (0..en.project.tracks.len()).collect()
                        } else {
                            o.tracks.clone()
                        };
                        for ti in ids {
                            let mut po = o.clone();
                            po.tracks = vec![ti];
                            let (part, tname) = {
                                let en = eng.lock().unwrap_or_else(|p| p.into_inner());
                                let p = &en.project;
                                let Some(t) = p.tracks.get(ti) else { continue };
                                po.title = format!("{} - {}", title, t.name);
                                (sheet::build(p, &po, &|t| crate::karaoke::track_syllables(p, t)), t.name.clone())
                            };
                            if part.tracks == 0 {
                                continue;
                            }
                            let safe: String = tname.chars().map(|c| if c.is_alphanumeric() || c == ' ' || c == '-' || c == '_' { c } else { '_' }).collect();
                            let tmp = dirp.join(format!(".{base}_part{ti}.html"));
                            std::fs::write(&tmp, sheet::html(&po.title, &part.pages, po.landscape, &css)).map_err(|e| format!("{}: {e}", tmp.display()))?;
                            let name = format!("{base} - {}.pdf", safe.trim());
                            let r = html_to_pdf(&tmp, &dirp.join(&name));
                            let _ = std::fs::remove_file(&tmp);
                            match r {
                                Ok(()) => out.push(Val::Str(name)),
                                Err(e) => {
                                    pdf_err = e;
                                    break;
                                }
                            }
                        }
                    }
                }
                if !pdf_err.is_empty() {
                    self.status = format!("Sheet music written, but the PDF failed: {pdf_err}");
                    return ok!(Val::List(out));
                }
                self.status = format!("Sheet music: {} bar(s), {} track(s), {} page(s) in {dir}", sheet.bars, sheet.tracks, n);
                ok!(Val::List(out))
            }
            _ => Err(format!("unknown builtin {name}")),
        }
    }
}

/// Chrome, Edge or Chromium: they print a page to PDF headlessly, vector text and fonts included.
pub fn find_browser() -> Option<std::path::PathBuf> {
    let mut c: Vec<std::path::PathBuf> = vec![];
    for var in ["ProgramFiles(x86)", "ProgramFiles", "LOCALAPPDATA"] {
        if let Some(r) = std::env::var_os(var) {
            let r = std::path::PathBuf::from(r);
            c.push(r.join("Microsoft/Edge/Application/msedge.exe"));
            c.push(r.join("Google/Chrome/Application/chrome.exe"));
            c.push(r.join("Chromium/Application/chrome.exe"));
            c.push(r.join("BraveSoftware/Brave-Browser/Application/brave.exe"));
        }
    }
    for p in ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge", "/Applications/Chromium.app/Contents/MacOS/Chromium"] {
        c.push(p.into());
    }
    if let Some(path) = std::env::var_os("PATH") {
        for d in std::env::split_paths(&path) {
            for n in ["google-chrome", "google-chrome-stable", "chromium", "chromium-browser", "microsoft-edge", "chrome", "msedge"] {
                c.push(d.join(n));
            }
        }
    }
    c.into_iter().find(|p| p.is_file())
}

/// print an HTML file to a PDF (the page size comes from the file's @page rule)
pub fn html_to_pdf(html: &std::path::Path, pdf: &std::path::Path) -> Result<(), String> {
    let exe = find_browser().ok_or("no Chrome / Edge / Chromium found to make the PDF; open the .html in a browser and print to PDF")?;
    let abs = std::fs::canonicalize(html).map_err(|e| format!("{}: {e}", html.display()))?;
    let mut p = abs.to_string_lossy().replace(char::from(92u8), "/");
    if let Some(r) = p.strip_prefix("//?/") {
        p = r.to_string();
    }
    let url = format!("file:///{}", p.trim_start_matches('/').replace(' ', "%20").replace('#', "%23"));
    let prof = std::env::temp_dir().join(format!("lsx_pdf_{}", std::process::id()));
    let _ = std::fs::remove_file(pdf);
    let mut cmd = std::process::Command::new(&exe);
    cmd.arg("--headless=new").arg("--disable-gpu").arg("--no-first-run").arg("--no-pdf-header-footer").arg("--run-all-compositor-stages-before-draw").arg("--virtual-time-budget=4000").arg(format!("--user-data-dir={}", prof.display())).arg(format!("--print-to-pdf={}", pdf.display())).arg(&url);
    cmd.stdout(std::process::Stdio::null()).stderr(std::process::Stdio::null());
    #[cfg(windows)]
    {
        use std::os::windows::process::CommandExt;
        cmd.creation_flags(0x08000000);
    }
    let mut child = cmd.spawn().map_err(|e| format!("{}: {e}", exe.display()))?;
    let t0 = std::time::Instant::now();
    let mut done = false;
    while t0.elapsed().as_secs() < 90 {
        if let Ok(Some(_)) = child.try_wait() {
            done = true;
            break;
        }
        // the PDF is written before the browser winds down
        if pdf.is_file() && std::fs::metadata(pdf).map(|m| m.len() > 0).unwrap_or(false) && t0.elapsed().as_secs() >= 2 {
            std::thread::sleep(std::time::Duration::from_millis(400));
            break;
        }
        std::thread::sleep(std::time::Duration::from_millis(100));
    }
    if !done {
        let _ = child.kill();
    }
    let _ = child.wait();
    let _ = std::fs::remove_dir_all(&prof);
    if pdf.is_file() && std::fs::metadata(pdf).map(|m| m.len() > 0).unwrap_or(false) {
        Ok(())
    } else {
        Err(format!("{} did not produce {}", exe.display(), pdf.display()))
    }
}

/// @font-face rules for the Daemon and Chesilin fonts found in `fdir`
pub fn fonts_css(fdir: &str) -> String {
    let mut fonts: Vec<(&str, Vec<u8>, &str)> = vec![];
    if let Ok(d) = std::fs::read(std::path::Path::new(fdir).join("Daemon.otf")) {
        fonts.push(("LSXDaemon", d, "font/otf"));
    }
    if let Ok(d) = std::fs::read(std::path::Path::new(fdir).join("Chesilin.ttf")) {
        fonts.push(("LSXChesilin", d, "font/ttf"));
    }
    sheet::font_css(&fonts)
}

← Back to index