mirror of
https://github.com/storytold/soundcraft.git
synced 2026-11-03 17:08:17 -06:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
98a6b2a96f | ||
|
|
f6bcc6cf6b |
@@ -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");
|
||||
|
||||
@@ -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)];
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user