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();
|
let mut entries = Vec::new();
|
||||||
|
|
||||||
for (index, file) in self.files.iter().enumerate() {
|
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
|
// 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
|
// 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+
|
// embed it as audio media — that both wastes space and loses the 5.1+
|
||||||
|
|
@ -1141,16 +1151,18 @@ impl AudioClipPool {
|
||||||
self.files.clear();
|
self.files.clear();
|
||||||
eprintln!("📊 [LOAD_SERIALIZED] Clear pool took {:.2}ms", clear_start.elapsed().as_secs_f64() * 1000.0);
|
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
|
// Size the pool to hold the highest pool_index (slots are addressed by index,
|
||||||
let max_index = entries.iter()
|
// so gaps are filled with placeholders). No entries → length 0, NOT 1: the old
|
||||||
.map(|e| e.pool_index)
|
// `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()
|
.max()
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
|
|
||||||
// Ensure we have space for all entries
|
// Ensure we have space for all entries
|
||||||
let resize_start = std::time::Instant::now();
|
let resize_start = std::time::Instant::now();
|
||||||
self.files.resize(max_index + 1, AudioFile::new(PathBuf::new(), Vec::new(), 2, 44100));
|
self.files.resize(pool_size, 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);
|
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() {
|
for (i, entry) in entries.iter().enumerate() {
|
||||||
let entry_start = std::time::Instant::now();
|
let entry_start = std::time::Instant::now();
|
||||||
|
|
|
||||||
|
|
@ -324,7 +324,11 @@ pub fn save_beam(
|
||||||
} else {
|
} else {
|
||||||
project_dir.join(rel)
|
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 size = std::fs::metadata(&full).map(|m| m.len()).unwrap_or(0);
|
||||||
let codec = full
|
let codec = full
|
||||||
.extension()
|
.extension()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue