Compare commits

...
3 changed files with 42 additions and 3 deletions
+27 -1
View File
@@ -212,6 +212,9 @@ impl Autosave {
let audio_dir = self.dir.join("Audio Files");
let mut s = session.clone();
let mut copies = BTreeMap::new();
// Copies made in this write, by the audio they hold: sources sharing one decoded buffer
// (a session may name one file under many ids) share one file.
let mut written: BTreeMap<*const SourceAudio, String> = BTreeMap::new();
for src in &mut s.sources {
let file = if Path::new(&src.path).is_absolute() { Some(PathBuf::from(&src.path)) } else { base.as_ref().map(|b| b.join(&src.path)) };
let audio = s.pool.get(src.id);
@@ -224,6 +227,7 @@ impl Autosave {
let Some(audio) = audio else { continue };
let name = match self.copies.remove(&src.id) {
Some((copy, name)) if copy.upgrade().is_some_and(|a| Arc::ptr_eq(&a, audio)) => name,
_ if written.contains_key(&Arc::as_ptr(audio)) => written.get(&Arc::as_ptr(audio)).cloned().unwrap_or_default(),
_ => {
let name = format!("{} {}.wav", sanitize_name(&src.name), self.next);
self.next += 1;
@@ -235,13 +239,16 @@ impl Autosave {
};
src.path = audio_dir.join(&name).to_string_lossy().into_owned();
src.unsaved = true;
written.insert(Arc::as_ptr(audio), name.clone());
copies.insert(src.id, (Arc::downgrade(audio), name));
}
let text = s.to_json().map_err(|err| EngineError::Io(err.to_string()))?;
write_atomically(&self.dir.join(ORIGIN), origin.unwrap_or_default().as_bytes())?;
write_atomically(&self.dir.join(SESSION), text.as_bytes())?;
for (_, name) in std::mem::replace(&mut self.copies, copies).into_values() {
let _ = std::fs::remove_file(audio_dir.join(name));
if !self.copies.values().any(|(_, kept)| *kept == name) {
let _ = std::fs::remove_file(audio_dir.join(name));
}
}
if let Some(r) = recording {
let plan = serde_json::to_string(r).map_err(|err| EngineError::Io(err.to_string()))?;
@@ -507,6 +514,25 @@ mod tests {
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn sources_sharing_one_buffer_share_one_copy() {
let dir = folder("autosave-aliases");
let mut e = Engine::default();
record(&mut e, 0.5);
// Nine more source ids over the same decoded audio, as a session file may declare.
let s = e.session_mut();
let (src, audio) = (s.sources[0].clone(), s.pool.get(s.sources[0].id).cloned().unwrap());
for _ in 0..9 {
let id = SourceId(s.alloc());
s.sources.push(soundcraft_model::Source { id, ..src.clone() });
s.pool.insert(id, audio.clone());
}
let mut a = Autosave::create(&dir.join("Autosave"), "Song").unwrap();
a.write(e.session(), None, None).unwrap();
assert_eq!(std::fs::read_dir(a.dir.join("Audio Files")).unwrap().count(), 1, "one file for the ten ids");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn a_take_recorded_after_undo_gets_its_own_copy() {
let dir = folder("autosave-undo");
+4 -1
View File
@@ -347,7 +347,10 @@ pub fn clip_id(p: &Value, cmd: &str) -> Result<ClipId> {
pub fn clip_ids_param(e: &Engine, p: &Value) -> Vec<ClipId> {
if let Some(arr) = p.get("clips").and_then(Value::as_array) {
return arr.iter().filter_map(Value::as_u64).map(ClipId).collect();
// Each clip once, in the order given: a repeated id would otherwise be processed (and,
// for a render, re-rendered into another source) once per repeat.
let mut seen = std::collections::HashSet::new();
return arr.iter().filter_map(Value::as_u64).map(ClipId).filter(|id| seen.insert(*id)).collect();
}
if let Some(n) = p.get("clip").and_then(Value::as_u64) {
return vec![ClipId(n)];
+11 -1
View File
@@ -418,7 +418,9 @@ fn svg_budgeted(parts: &[Part], setup: &ScoreSetup, budget: usize) -> Result<Str
const STAFF_GAP: f64 = 70.0;
const SYSTEM_GAP: f64 = 40.0;
let n_measures = parts.iter().map(|p| p.measures.len()).max().unwrap_or(0);
let per = setup.bars_per_system.max(1) as usize;
// The layout range the setup offers (1 to 16): a caller's own value can't multiply the
// barlines drawn per system beyond it.
let per = setup.bars_per_system.clamp(1, 16) as usize;
let systems = n_measures.div_ceil(per).max(1);
let label_w = if setup.show_track_names { 70.0 } else { 0.0 };
let staff_x0 = MARGIN + label_w + 34.0;
@@ -782,6 +784,14 @@ mod tests {
assert!(m.items.iter().any(|i| i.pitches.is_empty()), "the discarded suffix became rests");
}
#[test]
fn bars_per_system_is_held_to_the_supported_range() {
let setup = ScoreSetup { title: "t".into(), composer: String::new(), bars_per_system: u32::MAX, show_track_names: false };
let part = Part { name: "p".into(), bass: false, measures: vec![Measure { numerator: 4, denominator: 4, items: Vec::new() }] };
let out = svg_budgeted(std::slice::from_ref(&part), &setup, 1 << 20).unwrap();
assert_eq!(out.matches("stroke-width=\"1\"").count(), 16, "sixteen barlines, not billions");
}
#[test]
fn oversized_title_is_refused_by_the_header_check() {
// An oversized part name/title must be caught by the header check even with no measures: