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/<id>.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.
This commit is contained in:
parent
5f0d5354ed
commit
34eee3a620
|
|
@ -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/<id>.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<Uuid, u32>` | 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/<uuid>.png` — raster keyframe pixels, named by
|
||||
`RasterKeyframe.media_path`.
|
||||
- `media/raster/<uuid>.png` — raster keyframe pixels, named `media/raster/<id>.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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<AudioPoolEntry>,
|
||||
layer_to_track_map: &std::collections::HashMap<uuid::Uuid, u32>,
|
||||
thumbnail_blobs: &std::collections::HashMap<uuid::Uuid, Vec<u8>>,
|
||||
_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<LoadedProject, String> {
|
|||
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/<uuid>.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<LoadedProject, String> {
|
|||
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(_) => {
|
||||
|
|
|
|||
|
|
@ -111,8 +111,6 @@ pub struct RasterKeyframe {
|
|||
pub time: f64,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
/// ZIP-relative path: `"media/raster/<uuid>.png"`
|
||||
pub media_path: String,
|
||||
/// Stroke history (for potential replay / future non-destructive editing)
|
||||
pub stroke_log: Vec<StrokeRecord>,
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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(()) => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue