L
ling-daw
↓ 0 downloadsLing DAW engine: synth, physical-modeling drums, effects, mixer, sequencer, project I/O
lingfu install ling-dawCode / 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)
}