From 34eee3a620915d5092ac59e89813403389f50916 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Sun, 21 Jun 2026 23:21:39 -0400 Subject: [PATCH] Fix .beam save quirks: real sample rate, drop dead fields Address the code smells flagged in the .beam format spec: - Write the project's actual sample rate on save instead of a hardcoded 48000 (add AudioProject::sample_rate()). - Remove the vestigial RasterKeyframe.media_path field (it was only used by the legacy ZIP loader, which now derives "media/raster/.png" from the keyframe id) and the dead buffer_path_at_time accessor. Backward-compatible: older files carrying media_path deserialize fine (the field is ignored). - Drop the unused SaveSettings fields auto_embed_threshold_bytes / force_embed_all / force_link_all; only large_media_mode was ever consulted. Un-prefix the now-used `settings` parameter. Update BEAM_FILE_FORMAT.md to match. The remaining notes (reserved MediaKind::Video, exact-match version check) are design choices, left as-is. --- BEAM_FILE_FORMAT.md | 23 +++++++--------- daw-backend/src/audio/project.rs | 5 ++++ .../lightningbeam-core/src/file_io.rs | 27 +++++++------------ .../lightningbeam-core/src/raster_layer.rs | 8 ------ .../lightningbeam-editor/src/main.rs | 1 - 5 files changed, 24 insertions(+), 40 deletions(-) diff --git a/BEAM_FILE_FORMAT.md b/BEAM_FILE_FORMAT.md index 91a0e75..0fbdb6c 100644 --- a/BEAM_FILE_FORMAT.md +++ b/BEAM_FILE_FORMAT.md @@ -255,15 +255,15 @@ Collections that carry media linkage are **bold**: `RasterKeyframe` (within the layer tree) has `id: Uuid` (= its full-res `Raster` media id and the seed for its `RasterProxy` id), `time`, `width`, `height`, -`tween_after`, `stroke_log`, and a `media_path` string that is **vestigial** in -SQLite files (it names the legacy ZIP entry path; the SQLite path keys on `id` and -ignores it). Pixel buffers are `#[serde(skip)]` and faulted in from media rows. +`tween_after`, and `stroke_log`. Pixel buffers are `#[serde(skip)]` and faulted in +from media rows. (The legacy ZIP entry path `media/raster/.png` is derived from +`id`; older files may also carry a now-ignored `media_path` field.) ### 8.3 `SerializedAudioBackend` | Field | Type | Notes | |----------------------|---------------------|-------| -| `sample_rate` | `u32` | **Unreliable:** the current writer hardcodes `48000` here; authoritative rates are on `AudioProject.sample_rate` and each `AudioPoolEntry.sample_rate`. | +| `sample_rate` | `u32` | The project's system sample rate (mirrors `AudioProject.sample_rate`). Per-file rates are on each `AudioPoolEntry.sample_rate`. | | `project` | `AudioProject` | Tracks + MIDI clip pool (§8.4). | | `audio_pool_entries` | `[AudioPoolEntry]` | Audio source registry (§8.5). | | `layer_to_track_map` | `map` | UI layer UUID → engine track id. `#[serde(default)]`. | @@ -388,8 +388,8 @@ internal layout: extension. **FLAC entries are decoded to PCM `f32` and re-encoded in memory as IEEE-float WAV (WAV format tag 3)** before being base64-embedded; other formats are embedded as-is. The entry's `relative_path` is cleared after extraction. -- `media/raster/.png` — raster keyframe pixels, named by - `RasterKeyframe.media_path`. +- `media/raster/.png` — raster keyframe pixels, named `media/raster/.png` + after the keyframe's `id`. A reader MUST NOT modify a legacy ZIP on open; it loads into memory only. **The next save migrates the project to SQLite** (the save sees a non-SQLite target, so it takes @@ -451,18 +451,15 @@ A conforming **writer** MUST: These reflect the current reference implementation and are flagged for implementers and future spec revisions: -- `SerializedAudioBackend.sample_rate` is hardcoded to `48000` on save and SHOULD be - ignored in favour of `AudioProject.sample_rate` / `AudioPoolEntry.sample_rate`. - `MediaKind::Video` (`1`) is defined but never written; video media is always an external file via `VideoClip.file_path`. -- `RasterKeyframe.media_path` is serialized but unused by the SQLite path (vestigial - legacy-ZIP linkage). -- The reference writer's `SaveSettings` fields `auto_embed_threshold_bytes`, - `force_embed_all`, and `force_link_all` are not consulted by the current save path; - only the large-media mode is. - The project `version` check is exact-match with no compatibility window; a future revision should define semantic-version tolerance before bumping it. +(Earlier revisions of this section flagged a hardcoded save sample rate, a vestigial +`RasterKeyframe.media_path` field, and unused `SaveSettings` fields; these have since +been fixed/removed.) + ## 16. Inspecting a `.beam` file Because the container is plain SQLite, any `.beam` (SQLite variant) can be inspected diff --git a/daw-backend/src/audio/project.rs b/daw-backend/src/audio/project.rs index d301445..4cce4a1 100644 --- a/daw-backend/src/audio/project.rs +++ b/daw-backend/src/audio/project.rs @@ -41,6 +41,11 @@ impl Project { } } + /// The project's system sample rate in Hz. + pub fn sample_rate(&self) -> u32 { + self.sample_rate + } + /// Generate a new unique track ID fn next_id(&mut self) -> TrackId { let id = self.next_track_id; diff --git a/lightningbeam-ui/lightningbeam-core/src/file_io.rs b/lightningbeam-ui/lightningbeam-core/src/file_io.rs index c272537..335b22f 100644 --- a/lightningbeam-ui/lightningbeam-core/src/file_io.rs +++ b/lightningbeam-ui/lightningbeam-core/src/file_io.rs @@ -88,15 +88,6 @@ impl Default for LargeMediaMode { /// Settings for saving a project #[derive(Debug, Clone)] pub struct SaveSettings { - /// Automatically embed files smaller than this size (in bytes) - pub auto_embed_threshold_bytes: u64, - - /// Force embedding all media files - pub force_embed_all: bool, - - /// Force linking all media files (don't embed any) - pub force_link_all: bool, - /// How to store files at/above [`LARGE_MEDIA_THRESHOLD`] (pack vs reference). /// `Ask` behaves as `Reference` here (safe default: don't bloat the DB). pub large_media_mode: LargeMediaMode, @@ -105,9 +96,6 @@ pub struct SaveSettings { impl Default for SaveSettings { fn default() -> Self { Self { - auto_embed_threshold_bytes: 10_000_000, // 10 MB - force_embed_all: false, - force_link_all: false, large_media_mode: LargeMediaMode::Ask, } } @@ -269,7 +257,7 @@ pub fn save_beam( audio_pool_entries: Vec, layer_to_track_map: &std::collections::HashMap, thumbnail_blobs: &std::collections::HashMap>, - _settings: &SaveSettings, + settings: &SaveSettings, ) -> Result<(), String> { let fn_start = std::time::Instant::now(); eprintln!("📊 [SAVE_BEAM] Starting save_beam() (SQLite container)..."); @@ -349,7 +337,7 @@ pub fn save_beam( // (`Ask` == reference); smaller files are always packed. let reference_it = entry.is_video_audio || (size >= LARGE_MEDIA_THRESHOLD - && _settings.large_media_mode != LargeMediaMode::Pack); + && settings.large_media_mode != LargeMediaMode::Pack); if reference_it { referenced = Some(rel.clone()); } else { @@ -498,7 +486,7 @@ pub fn save_beam( modified: now.clone(), ui_state: document.clone(), audio_backend: SerializedAudioBackend { - sample_rate: 48000, // TODO: Get from audio engine + sample_rate: audio_project.sample_rate(), project: audio_project.clone(), audio_pool_entries: modified_entries, layer_to_track_map: layer_to_track_map.clone(), @@ -879,8 +867,11 @@ fn load_beam_zip_legacy(path: &Path) -> Result { for layer in document.root.children.iter_mut() { if let crate::layer::AnyLayer::Raster(rl) = layer { for kf in &mut rl.keyframes { - if !kf.media_path.is_empty() { - match zip.by_name(&kf.media_path) { + // Legacy ZIP raster entries are named "media/raster/.png", + // derivable from the keyframe id (the old `media_path` field). + let entry_path = format!("media/raster/{}.png", kf.id); + { + match zip.by_name(&entry_path) { Ok(mut png_file) => { let mut png_bytes = Vec::new(); let _ = png_file.read_to_end(&mut png_bytes); @@ -890,7 +881,7 @@ fn load_beam_zip_legacy(path: &Path) -> Result { kf.raw_pixels = rgba.into_raw(); raster_load_count += 1; } - Err(e) => eprintln!("⚠️ [LOAD_BEAM] Failed to decode raster PNG {}: {}", kf.media_path, e), + Err(e) => eprintln!("⚠️ [LOAD_BEAM] Failed to decode raster PNG {}: {}", entry_path, e), } } Err(_) => { diff --git a/lightningbeam-ui/lightningbeam-core/src/raster_layer.rs b/lightningbeam-ui/lightningbeam-core/src/raster_layer.rs index b4c858a..6a14d11 100644 --- a/lightningbeam-ui/lightningbeam-core/src/raster_layer.rs +++ b/lightningbeam-ui/lightningbeam-core/src/raster_layer.rs @@ -111,8 +111,6 @@ pub struct RasterKeyframe { pub time: f64, pub width: u32, pub height: u32, - /// ZIP-relative path: `"media/raster/.png"` - pub media_path: String, /// Stroke history (for potential replay / future non-destructive editing) pub stroke_log: Vec, pub tween_after: TweenType, @@ -157,13 +155,11 @@ impl RasterKeyframe { pub fn new(time: f64, width: u32, height: u32) -> Self { let id = Uuid::new_v4(); - let media_path = format!("media/raster/{}.png", id); Self { id, time, width, height, - media_path, stroke_log: Vec::new(), tween_after: TweenType::Hold, raw_pixels: Vec::new(), @@ -264,10 +260,6 @@ impl RasterLayer { .map(|pos| self.keyframes.remove(pos)) } - /// Return the ZIP-relative PNG path for the active keyframe at `time`, or `None`. - pub fn buffer_path_at_time(&self, time: f64) -> Option<&str> { - self.keyframe_at(time).map(|kf| kf.media_path.as_str()) - } } // Delegate all LayerTrait methods to self.layer diff --git a/lightningbeam-ui/lightningbeam-editor/src/main.rs b/lightningbeam-ui/lightningbeam-editor/src/main.rs index 09b04c6..e4f94b4 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/main.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/main.rs @@ -731,7 +731,6 @@ impl FileOperationsWorker { let settings = SaveSettings { large_media_mode, - ..SaveSettings::default() }; match save_beam(&path, &document, &mut audio_project, audio_pool_entries, layer_to_track_map, &thumbnail_blobs, &settings) { Ok(()) => {