Commit Graph

124 Commits

Author SHA1 Message Date
Skyler Lehmkuhl 6e6feaddf5 Fix FLAC export end-to-end (real backend path) + smart tag defaults
The prior audio-tags commit put real FLAC + metadata into export/audio_exporter.rs
— which turned out to be dead code (declared, never called; whole file was
EngineController::start_export_audio → daw-backend's export_audio, which still
routed FLAC to the erroring hound stub — hence "not implemented in daw-backend".

Move the work to where export actually happens:
- daw-backend/src/audio/export.rs: real ffmpeg FLAC (16-bit S16 / 24-bit S32,
  skipping the trailing empty flush packet the FLAC muxer rejects); apply_metadata
  on MP3/AAC/FLAC output; RIFF LIST/INFO chunk appended to WAV. New metadata field
  on the backend ExportSettings, threaded from the UI in run_audio_export. Tests
  assert real fLaC magic + round-tripped tags, and a valid WAV INFO chunk.
- Delete the dead export/audio_exporter.rs (removes the duplicate FLAC impl).

Smart tag defaults (filled only when empty, never clobbering edits):
- Year → current civil year, computed from the system clock with i64 math (no
  date crate; correct past 2038/2106 — tests cover post-i32/u32 timestamps).
- Artist → last-used value, else the OS username ($USER/%USERNAME%).
- Album → last-used value.
Last-used Artist/Album persist in AppConfig and prefill next export.
2026-07-09 13:09:54 -04:00
Skyler Lehmkuhl 15bdf80ec1 Audio export: real FLAC + tag metadata for all formats
- FLAC is now real FLAC via ffmpeg, not WAV bytes in a .flac file. 16-bit uses
  S16, 24-bit uses S32 (ffmpeg's flac encoder emits bits_per_raw_sample=24).
  The flush emits a trailing empty packet that the FLAC muxer rejects as
  "invalid data" — it's skipped.

- Tag metadata (title/artist/album/genre/year/track/comment) written into every
  format via each container's native tags: ID3v2 (MP3), MP4 atoms (M4A), Vorbis
  comments (FLAC) set through ffmpeg's output metadata; RIFF LIST/INFO appended
  to the hound-written WAV (with a fixed-up RIFF size). New AudioMetadata type
  on AudioExportSettings; dialog gains a Tags section and defaults Title to the
  project name.

Tests: FLAC is a real fLaC container with round-tripped tags; WAV keeps a valid
RIFF with a working INFO chunk.
2026-07-09 12:52:37 -04:00
Skyler Lehmkuhl 77eb2c2d33 Fix oscillator/synth phase wraparound drift
Replace conditional-subtraction phase wrapping with a single stable wrap:
- OscillatorNode: `(phase + freq_mod/sr).rem_euclid(1.0)` — also wraps correctly
  when FM drives freq_mod negative (the old `if >= 1.0` wouldn't).
- SynthVoice: `(phase + frequency/sr).fract()`.
Repeated `if phase >= 1.0 { phase -= 1.0 }` accumulates f32 rounding error over
long-held notes, drifting the timbre.
2026-07-02 01:15:45 -04:00
Skyler Lehmkuhl b301c63387 Address code-review findings across export, decode, and data model
Export correctness:
- Honor the user's color-range (Limited/Full) on the software encode path:
  thread full_range through gpu_yuv (new shader range uniform), the CPU
  swscale fallback (sws_setColorspaceDetails → BT.709 + range, fixing the
  BT.601 hue/level shift on odd-width exports), and the encoder color tags.
- Reject HDR + WebM up front with a clear message and log the forced HEVC
  override instead of producing an unplayable file.
- Delete dead render_frame_to_rgba_hdr (hardcoded Stretch; live HDR path
  already honors the fit mode).

Decode/playback (video.rs):
- Drain the decoder at EOF (send_eof + flush) so the final B-frame-delayed
  frames render instead of erroring; per-frame logic extracted to a helper.
- Missing-PTS frames continue monotonically rather than snapping to ts=0.
- Force exact thumbnail width so sub-128px sources aren't shown stretched.

Resource leaks (gpu-video-encoder):
- dmabuf import_raw: RAII guard frees the duped fd + partial VkImages/memory
  on every error path.
- vaapi alloc: free device/frames-ctx/AVFrames on the unexpected-DRM path.

Data model / robustness:
- collapse_boundary_spikes requires a full curve reversal (all control
  points) so it no longer deletes a real lens/sliver and drops the fill.
- Export audio spin-wait ignores a stale `finished` flag when a forward
  seek is pending (was rendering silence over real audio).
- RasterDiff apply_before/after take current dims and skip on a post-resize
  mismatch.
- beam_archive read_media_full caps the preallocation from untrusted total_len.

UI/visual:
- SVG export skips hidden layers/empty groups; import folds fill-opacity into
  gradients and surfaces failures as a notification.
- Active raster-layer border uses playback_time + overlay_transform.
- gpu_brush remove_layer_texture also evicts the stale low-res proxy.
- ensure_raster_resident_for_undo registers faulted frames in the LRU so
  resident RAM stays bounded.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 17:47:32 -04:00
Skyler Lehmkuhl 70ac0cde00 Fix audio export hang when a video's audio is shorter than the video
Offline export blocks each streaming source until decoded frames are
available. For a video whose audio track ends before the video (or a
container like WebM/Opus with no exact stream duration), the tail chunks
wait for frames that never arrive — the 10s safety valve fires per chunk,
so the export appears to hang indefinitely.

Add a `finished` flag to ReadAheadBuffer, set when the disk reader hits
EOF (cleared on seek). The export-mode wait now breaks immediately once
the source is finished and the requested frames aren't present, rendering
silence for the missing tail instead of timing out chunk-by-chunk. Only
the export path reads the flag, so playback is unaffected.
2026-06-23 19:06:21 -04:00
Skyler Lehmkuhl ce151ffd61 Pack video into .beam and stream frames + audio from the blob
Video was the only media type always kept external (VideoClip.file_path),
so a project with video wasn't self-contained. Now video packs into the
SQLite container under the same large-media policy as audio (pack < 2 GB
unless the user chose Reference), and both the frames and the embedded audio
track decode by streaming directly from the blob — no temp files.

- New crate ffmpeg-blob-io: an AVIOContext-over-Read+Seek shim (BlobInput)
  that lets ffmpeg demux from an arbitrary byte source. Isolates all the
  unsafe FFI + ffmpeg ABI coupling (version-pinned =8.0.0/=8.0.1). Manual
  Drop teardown order; AVSEEK_SIZE restores the read position (FFmpeg assumes
  a size query doesn't move it — required for MP4 moov-at-end).
- Schema/save/load: VideoClip.media_id; save_beam packs/references video as
  MediaKind::Video (keyed by clip id); load resolves packed vs referenced and
  reports missing sources. A packed clip points its linked video-audio pool
  entry's media_id at the video row so the audio streams from the same blob.
- Frames: video.rs VideoSource{Path,Packed} threaded through new/seek/scan/
  probe/thumbnails (a fresh BlobReader per open); editor builds the source
  from current_file_path (now set before register_loaded_videos).
- Audio: VideoAudioReader::open_source via BlobInput; the disk_reader
  StreamSource block on packed video-audio is removed; the engine's existing
  factory activation routes it unchanged.

Tests: ffmpeg-blob-io AVIO unit tests (WAV via Cursor, seek, open/drop loop);
core packed_video_stream (blob->AVIO->Input) and beam_archive video round-trip;
daw-backend open_source test (compiles; links/runs only off-container).
Runtime-verified: a packed video plays frames + audio after the source file
is removed.
2026-06-22 09:17:10 -04:00
Skyler Lehmkuhl 34eee3a620 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.
2026-06-21 23:21:48 -04:00
Skyler Lehmkuhl 83057b754a Clean up build warnings
Resolve all compiler warnings across daw-backend, lightningbeam-core, and
lightningbeam-editor:

- Delete dead code: the superseded CPU raster tools in raster_tool.rs
  (EffectBrush/Smudge/Gradient/Transform/Warp/Liquify/Selection — replaced by
  the GPU path), plus orphaned helpers and never-read struct fields.
- Mechanical fixes: drop unused imports/variables/mut, underscore unused params,
  `drop(&x)` -> `let _ = x`, deprecated egui::Rounding -> CornerRadius, snake_case
  rename, elided-lifetime Cow<'_, [u8]>.
- Keep the WIP CSS theming system (theme.rs/theme_render.rs) under
  #[allow(dead_code)] rather than deleting it.

Editor checks warning-free; 293 core tests pass.
2026-06-21 16:48:59 -04:00
Skyler Lehmkuhl d001ca1083 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.
2026-06-20 18:46:19 -04:00
Skyler Lehmkuhl 6d386a884e Phase 5: fix broken unit tests; cargo test --lib green again
The lib unit tests had gone stale (time values became newtypes) and no longer
compiled. Updated the test code to the current API and fixed the few real issues
the now-running tests surfaced.

Test-only:
- Wrap raw f64 time literals in Beats(...) where the API now takes Beats
  (automation.rs); pass &TempoMap / Beats where signatures changed (clip.rs,
  effect_layer.rs).
- shape.rs: assert the documented no-fill default (fill_color None) instead of Some.
- add_clip_instance / trim_clip_instances tests: register a vector clip with the
  test's clip_id so the action's get_clip_duration lookup succeeds.

Production fix (delete_folder.rs):
- DeleteFolderAction(MoveToParent) reparented child subfolders to the deleted
  folder's parent but never restored them on undo, orphaning them. Track the moved
  subfolder ids and restore their parent on rollback.

Result: daw-backend lib 17 passed; lightningbeam-core lib 264 passed.
2026-06-19 16:45:23 -04:00
Skyler Lehmkuhl 4ad95e6755 Downmix surround to stereo + reload video audio via FFmpeg
Surround → stereo downmix:
- render_from_file folds multichannel sources (5.1/7.1/…) down to stereo with
  proper coefficients (full level for the matching front channel, 1/√2 for centre
  + each surround, LFE dropped), normalized per row to avoid clipping (matching
  ffmpeg's default). Applied uniformly to both the direct-copy and sinc-resample
  paths and to every storage type (PCM, compressed, video audio), only when
  dst==2 && src>2; unknown layouts fall back to front L/R. Previously it just took
  FL/FR, dropping centre dialog + surrounds.

Proper video-audio reload:
- A video's audio track is now stored as a path reference to the video (never
  packed/embedded as audio media) and re-probed via FFmpeg on load into a
  streaming VideoAudio entry, so multichannel audio survives reload (the old
  Symphonia reconstitution collapsed it, breaking the downmix). Driven by a new
  AudioPoolEntry.is_video_audio flag across serialize / save_beam / load. Also
  removes the decode-whole-video-to-RAM + temp-file path on load.

Fix video scaling:
- Any video with dimensions larger than the stage was being scaled down into the corner incorrectly; we now bake the frame-clip scale into the instance transform.
2026-06-17 18:30:42 -04:00
Skyler Lehmkuhl c784816615 Phase 2: bound video frame cache + stream the export mux
- VideoManager.frame_cache: unbounded HashMap (grew per distinct frame during
  playback) -> LruCache evicted by a 256MB byte budget. Byte-budget rather than
  frame count is robust across resolutions (a 4K frame is ~33MB vs ~2MB at
  800x600). unload_video pops per-clip keys (LruCache has no retain).
- mux_video_and_audio: stream-merge the two inputs by PTS with one pending
  packet per stream (O(1) memory) instead of collecting every packet into Vecs
  first (O(duration)). Output is byte-identical.
- export AAC: sanitize the planar-f32 path (non-finite -> 0, finite clamped to
  [-1,1]) like the integer paths, with a one-time warning. A stray NaN/Inf
  render sample no longer fails the whole export.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 14:30:32 -04:00
Skyler Lehmkuhl 3d7cff9ad0 Stream audio & video from .beam container; waveform LOD pyramid + persistence
Migrate the .beam container to SQLite and stream media from it instead of
decoding whole files into RAM on import/load.

Container & large files:
- SQLite .beam container (beam_archive) with in-place transactional saves and an
  incremental BlobReader; supports both packed (chunked blobs) and referenced
  (external path) media, with a user preference + first-import prompt for files
  over the large-media threshold.

Audio streaming:
- Stream packed compressed audio on load via an inversion-of-control blob factory
  (AudioBlobSourceFactory): daw-backend defines the trait, core implements it
  over BlobReader, so the audio engine stays container-agnostic.
- Bulk-activate disk streaming for all loaded clips after SetProject.
- Sample-accurate compressed seek (SeekMode::Accurate; Coarse mislands on VBR).

Video:
- Video frames decoded/streamed on demand; thumbnails generated asynchronously
  on a dedicated decoder so import/load never blocks the UI.
- The video's audio track is streamed on demand via an ffmpeg VideoAudioReader
  as a separate editable AudioClip (no /tmp WAV extraction).

Waveform overview:
- Streaming min/max LOD pyramid (waveform_pyramid), bounded memory, configurable
  floor B; serialized into the container and restored on load (or generated in
  the background from the packed blob when absent), so no re-decode on reload.
- GPU min/max upload path; integer-LOD textureLoad fixes zoom-dependent wobble.
2026-06-17 13:52:38 -04:00
Skyler Lehmkuhl 54d5764bd0 Make beats canonical representation rather than seconds 2026-06-02 13:06:36 -04:00
Skyler Lehmkuhl f372a84313 Massive tempo refactor - make beats canonical time rep and allow them to be non constant 2026-04-02 10:26:01 -04:00
Skyler Lehmkuhl ae146533d9 Update automation lanes too 2026-04-01 10:17:52 -04:00
Skyler Lehmkuhl 3fc4773ec3 Fix remaining sites that weren't updating properly on BPM changes 2026-04-01 09:33:35 -04:00
Skyler Lehmkuhl cfb8e4462b All events now have three time references for seconds, measures/beats, frames 2026-03-30 10:15:55 -04:00
Skyler Lehmkuhl 65a550d8f4 Add piano roll note snapping 2026-03-24 19:24:24 -04:00
Skyler Lehmkuhl fa40173562 Add automatable volume and pan control to default instruments 2026-03-23 23:27:05 -04:00
Skyler Lehmkuhl 121fa3a50a Add count-in 2026-03-20 20:51:50 -04:00
Skyler Lehmkuhl c938ea44b0 Add metronome 2026-03-19 01:16:26 -04:00
Skyler Lehmkuhl 6b6ae230a1 Add pitch bend support 2026-03-18 23:11:24 -04:00
Skyler Lehmkuhl 4f3da810d0 Add automation inputs for audio graphs 2026-03-18 11:25:48 -04:00
Skyler Lehmkuhl be8514e2e6 Fix midi tracks recording previews 2026-03-11 12:53:26 -04:00
Skyler Lehmkuhl 3bc980d08d Use audio engine as source of truth for audio tracks 2026-03-11 12:37:31 -04:00
Skyler Lehmkuhl 7a3f522735 Give metatracks explicit node graphs 2026-03-10 20:20:46 -04:00
Skyler Lehmkuhl ac2b4ff8ab Improve idle performance 2026-03-10 02:41:44 -04:00
Skyler Lehmkuhl 4e79abdc35 bundle amp models more sensibly 2026-03-04 15:20:20 -05:00
Skyler Lehmkuhl b4c7a45990 fix NAM model loading 2026-03-02 11:58:13 -05:00
Skyler Lehmkuhl 49b822da8c Add final mix VU meters 2026-03-01 15:04:58 -05:00
Skyler Lehmkuhl a6e04ae89b Add VU meters 2026-03-01 14:49:49 -05:00
Skyler Lehmkuhl 5a19e91788 add vibrato node 2026-02-26 19:14:34 -05:00
Skyler Lehmkuhl eab116c930 Add beat mode 2026-02-22 18:43:17 -05:00
Skyler Lehmkuhl 4122fda954 piano roll and clip resizing fixes 2026-02-21 18:45:46 -05:00
Skyler Lehmkuhl 7e3f18c95b Add amp sim 2026-02-21 09:43:03 -05:00
Skyler Lehmkuhl 3eba231447 deduplicate node list 2026-02-21 09:42:05 -05:00
Skyler Lehmkuhl 84f1f8e7d7 Add orchestral sampled instruments 2026-02-21 07:28:19 -05:00
Skyler Lehmkuhl 469849a0d6 Add nested audio tracks 2026-02-21 03:56:07 -05:00
Skyler Lehmkuhl 66c848e218 Multi sample bulk import 2026-02-20 01:59:37 -05:00
Skyler Lehmkuhl 35089f3b2e Add state-variable filter 2026-02-20 00:20:59 -05:00
Skyler Lehmkuhl c344e11e42 Code cleanup 2026-02-19 11:19:44 -05:00
Skyler Lehmkuhl a98b59a6d3 Clean up build warnings 2026-02-19 10:21:48 -05:00
Skyler Lehmkuhl c66487b25e Add script node 2026-02-19 09:29:14 -05:00
Skyler Lehmkuhl 75be94d737 Add step sequencer node 2026-02-19 06:06:41 -05:00
Skyler Lehmkuhl dae82b02d1 Merge branch 'rust-ui' of https://git.skyler.io/skyler/Lightningbeam into rust-ui 2026-02-19 05:56:00 -05:00
Skyler Lehmkuhl 0a18d28f98 Add arpeggiator node 2026-02-19 05:30:34 -05:00
Skyler Lehmkuhl 89bbd3614f Add beat node 2026-02-19 01:19:40 -05:00
Skyler Lehmkuhl 21a49235fc sampler improvements, live waveform preview 2026-02-17 10:08:49 -05:00
Skyler Lehmkuhl 9935c2f3bd resample recorded audio if it has a different sample rate (fix for wsapi persnicketiness) 2026-02-17 09:44:24 -05:00