Fix save crash on zero/sparse-audio projects (phantom pool placeholder)
`AudioPool::load_from_serialized` sizes the slot Vec by pool_index and fills gaps
with empty `AudioFile::new(PathBuf::new(), …)` placeholders. Two bugs let a
placeholder reach the next save and abort it with "Is a directory":
- Off-by-one: `entries.max().unwrap_or(0) + 1` made an *empty* pool length 1, so a
project with no audio still got one placeholder. Size by `max(pool_index + 1)`
→ empty entries yield length 0.
- `serialize()` emitted placeholder slots: an empty path round-trips to
`relative_path = Some("")`, which `save_beam` resolves to the project directory
(`join("")`) and tries to read as media. Skip empty-path / no-packed-media slots.
- Defense in `save_beam`: gate referenced-media packing on `full.is_file()` (not
`exists()`), so any blank/dir path falls through to embedded data instead of
reading a directory.
Pre-existing; surfaced by a save → reload → save cycle on a raster-only project.
This commit is contained in:
parent
39dc402ba3
commit
d001ca1083
|
|
@ -926,6 +926,16 @@ impl AudioClipPool {
|
|||
let mut entries = Vec::new();
|
||||
|
||||
for (index, file) in self.files.iter().enumerate() {
|
||||
// Skip placeholder pool slots: `load_from_serialized` resizes the pool to
|
||||
// `max_index + 1` filled with empty `AudioFile::new(PathBuf::new(), …)` to
|
||||
// cover index gaps (and creates one even when there are no entries at all).
|
||||
// Such a slot has an empty path and no packed media — there's nothing to
|
||||
// persist, and emitting it yields an entry whose empty `relative_path`
|
||||
// resolves to the project directory itself (unreadable on the next save).
|
||||
if file.path.as_os_str().is_empty() && file.packed_media_id.is_none() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Video's audio track: reference the video file (it's also referenced
|
||||
// by the VideoClip) and re-probe it via FFmpeg on load. Never pack or
|
||||
// embed it as audio media — that both wastes space and loses the 5.1+
|
||||
|
|
@ -1141,16 +1151,18 @@ impl AudioClipPool {
|
|||
self.files.clear();
|
||||
eprintln!("📊 [LOAD_SERIALIZED] Clear pool took {:.2}ms", clear_start.elapsed().as_secs_f64() * 1000.0);
|
||||
|
||||
// Find the maximum pool index to determine required size
|
||||
let max_index = entries.iter()
|
||||
.map(|e| e.pool_index)
|
||||
// Size the pool to hold the highest pool_index (slots are addressed by index,
|
||||
// so gaps are filled with placeholders). No entries → length 0, NOT 1: the old
|
||||
// `max().unwrap_or(0) + 1` produced a spurious placeholder for an empty pool.
|
||||
let pool_size = entries.iter()
|
||||
.map(|e| e.pool_index + 1)
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
|
||||
// Ensure we have space for all entries
|
||||
let resize_start = std::time::Instant::now();
|
||||
self.files.resize(max_index + 1, AudioFile::new(PathBuf::new(), Vec::new(), 2, 44100));
|
||||
eprintln!("📊 [LOAD_SERIALIZED] Resize pool to {} took {:.2}ms", max_index + 1, resize_start.elapsed().as_secs_f64() * 1000.0);
|
||||
self.files.resize(pool_size, AudioFile::new(PathBuf::new(), Vec::new(), 2, 44100));
|
||||
eprintln!("📊 [LOAD_SERIALIZED] Resize pool to {} took {:.2}ms", pool_size, resize_start.elapsed().as_secs_f64() * 1000.0);
|
||||
|
||||
for (i, entry) in entries.iter().enumerate() {
|
||||
let entry_start = std::time::Instant::now();
|
||||
|
|
|
|||
|
|
@ -324,7 +324,11 @@ pub fn save_beam(
|
|||
} else {
|
||||
project_dir.join(rel)
|
||||
};
|
||||
if full.exists() {
|
||||
// Require an actual file: an empty/blank `relative_path` resolves to the
|
||||
// project directory itself (`join("")` == dir), which `exists()` accepts
|
||||
// but can't be read as media. `is_file()` skips dirs + missing paths, so
|
||||
// such an entry correctly falls through to embedded data below.
|
||||
if full.is_file() {
|
||||
let size = std::fs::metadata(&full).map(|m| m.len()).unwrap_or(0);
|
||||
let codec = full
|
||||
.extension()
|
||||
|
|
|
|||
Loading…
Reference in New Issue