mirror of
https://github.com/storytold/soundcraft.git
synced 2026-11-09 11:50:40 -06:00
Merge pull request #1416 from ZIDAN44/fix/1409-web-export-clips
fix(engine): Bounce Stems and Export Clips work in the web build (#1409)
This commit is contained in:
+79
-15
@@ -178,8 +178,12 @@ pub fn stored_path(stored: &str, session: &Path) -> std::result::Result<PathBuf,
|
||||
craft_docpath::resolve_absolute(stored, Some(&session)).map_err(|why| format!("not opened: {why}"))
|
||||
}
|
||||
|
||||
/// Create `dir` and its parents, inside the automation write root when there is one.
|
||||
/// Create `dir` and its parents, inside the automation write root when there is one. The browser
|
||||
/// has no folders: there each file is delivered by its name ([`write_file`]) and this does nothing.
|
||||
pub(crate) fn make_dir(dir: impl AsRef<Path>) -> Result<()> {
|
||||
if cfg!(target_arch = "wasm32") {
|
||||
return Ok(());
|
||||
}
|
||||
let dir = dir.as_ref();
|
||||
// The empty folder of a bare file name ("t.scraft") is the working directory, which the root
|
||||
// check would refuse as an empty path.
|
||||
@@ -563,29 +567,48 @@ impl From<StripNames> for soundcraft_model::AutoName {
|
||||
}
|
||||
}
|
||||
|
||||
/// Write each clip as a WAV file named after it into `dir` (in the browser, delivered by name
|
||||
/// through [`write_file`]). Returns how many were written.
|
||||
pub fn export_clips(e: &Engine, ids: &[soundcraft_model::ClipId], dir: &str) -> Result<usize> {
|
||||
let s = e.session();
|
||||
make_dir(dir)?;
|
||||
let mut n = 0;
|
||||
let mut names = ClipNames::default();
|
||||
for id in ids {
|
||||
let Some((t, c)) = s.find_clip(*id) else { continue };
|
||||
let audio = soundcraft_mix::render_clips(s, t, c.range());
|
||||
let buf = AudioBuffer { sample_rate: s.sample_rate.hz(), channels: audio };
|
||||
let bytes = soundcraft_audio_io::encode(&buf, &EncodeOptions::default()).map_err(|err| EngineError::Io(err.to_string()))?;
|
||||
let stem = sanitize_name(&c.name);
|
||||
let mut path = Path::new(dir).join(format!("{stem}.wav"));
|
||||
let mut k = 2u32;
|
||||
while path.exists() {
|
||||
path = Path::new(dir).join(format!("{stem}-{k}.wav"));
|
||||
k = k.saturating_add(1);
|
||||
if k > 10_000 {
|
||||
return Err(EngineError::Io(format!("{dir}: too many files named {stem}")));
|
||||
}
|
||||
}
|
||||
write_path(&path, bytes)?;
|
||||
n += 1;
|
||||
let path = clip_file(dir, &sanitize_name(&c.name), &mut names, Path::exists)?;
|
||||
write_file(&path.to_string_lossy(), &bytes)?;
|
||||
}
|
||||
Ok(n)
|
||||
Ok(names.taken.len())
|
||||
}
|
||||
|
||||
/// The files an export has named so far, and for each name the suffix to try next, so a run of
|
||||
/// same-named clips doesn't count up from 2 again for every clip.
|
||||
#[derive(Default)]
|
||||
struct ClipNames {
|
||||
taken: std::collections::HashSet<PathBuf>,
|
||||
next: std::collections::HashMap<String, u32>,
|
||||
}
|
||||
|
||||
/// The first of `<stem>.wav`, `<stem>-2.wav`, … in `dir` that is neither `on_disk` nor already
|
||||
/// named by this export, recorded in `names`: the browser has no disk to look at, so same-named
|
||||
/// clips are kept apart by `names`.
|
||||
fn clip_file(dir: &str, stem: &str, names: &mut ClipNames, on_disk: impl Fn(&Path) -> bool) -> Result<PathBuf> {
|
||||
let file = |k: u32| Path::new(dir).join(if k < 2 { format!("{stem}.wav") } else { format!("{stem}-{k}.wav") });
|
||||
let mut k = names.next.get(stem).copied().unwrap_or(1);
|
||||
let mut path = file(k);
|
||||
while on_disk(&path) || names.taken.contains(&path) {
|
||||
k = k.saturating_add(1);
|
||||
if k > 10_000 {
|
||||
return Err(EngineError::Io(format!("{dir}: too many files named {stem}")));
|
||||
}
|
||||
path = file(k);
|
||||
}
|
||||
names.next.insert(stem.to_owned(), k.saturating_add(1));
|
||||
names.taken.insert(path.clone());
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
/// Write `session` to `path`; returns the session with updated media paths, the number of audio
|
||||
@@ -996,3 +1019,44 @@ mod stem_tests {
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod clip_tests {
|
||||
/// The browser has no disk to find earlier files on: same-named clips get their own names
|
||||
/// from the export itself, as they do from the folder natively (#1409).
|
||||
#[test]
|
||||
fn same_named_clips_get_their_own_names_with_nothing_on_disk() {
|
||||
let dir = std::env::temp_dir().join(format!("sc-clip-names-{}", std::process::id()));
|
||||
let mut names = super::ClipNames::default();
|
||||
for want in ["Take.wav", "Take-2.wav", "Take-3.wav"] {
|
||||
let path = super::clip_file(&dir.to_string_lossy(), "Take", &mut names, std::path::Path::exists).unwrap();
|
||||
assert_eq!(path, dir.join(want));
|
||||
}
|
||||
assert!(!dir.exists());
|
||||
}
|
||||
|
||||
/// A file already in the folder is still skipped, and a run of same-named clips picks up where
|
||||
/// the last one left off instead of looking at every earlier name again for each clip.
|
||||
#[test]
|
||||
fn same_named_clips_skip_files_on_disk_and_do_not_count_up_from_the_start_each_time() {
|
||||
let dir = std::env::temp_dir().join(format!("sc-clip-disk-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
std::fs::write(dir.join("Take-2.wav"), b"").unwrap();
|
||||
let looked = std::cell::Cell::new(0usize);
|
||||
let on_disk = |p: &std::path::Path| {
|
||||
looked.set(looked.get() + 1);
|
||||
p.exists()
|
||||
};
|
||||
let mut names = super::ClipNames::default();
|
||||
for want in ["Take.wav", "Take-3.wav", "Take-4.wav"] {
|
||||
let path = super::clip_file(&dir.to_string_lossy(), "Take", &mut names, on_disk).unwrap();
|
||||
assert_eq!(path, dir.join(want));
|
||||
}
|
||||
let before = looked.get();
|
||||
for _ in 0..1_000 {
|
||||
super::clip_file(&dir.to_string_lossy(), "Take", &mut names, on_disk).unwrap();
|
||||
}
|
||||
assert_eq!(looked.get() - before, 1_000);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user