Commit Graph

10 Commits

Author SHA1 Message Date
Skyler Lehmkuhl 07b2478a0e Merge take management into painting UX branch
Conflicts were almost all upstream's take work inserting code next to
lines our earlier cycle-recording commits had touched, plus one API
change we had to follow:

- ClipInstance trim access changed (ci.trim_start -> a ci_trim_start
  local; trim_end now needs .raw()). Took upstream's form throughout
  timeline.rs; the already-merged lines around each hunk were using it,
  so keeping ours would not have compiled.
- recording.rs wrap_at_cycle now bumps current_pass after
  close_active_notes, so a note held across the boundary has its tail
  attributed to the pass that's ending and its re-opened half to the one
  beginning. Upstream's version is a superset of ours.
- Kept our headers-only timeline mode (drop the track area below 1.5x the
  layer-header width); upstream had not touched that block.

Everything else (TRASH glyph, take actions, SetCycleMidiSeparateTakes,
engine cycle-MIDI plumbing, render_take_menu) was purely additive.

Workspace compiles; daw-backend's 32 tests pass, including the cycle and
MIDI take-slicing suites that sit directly on the resolved code.
2026-07-14 13:27:01 -04:00
Skyler Lehmkuhl 5e73f7f75d Painting UX: unified color/brush tools, tablet buttons, layer opacity
Addresses a batch of issues found during a painting test.

Tools
- Paint bucket used the background color unconditionally. It now picks
  FG/BG like the brush does, via a shared Color: [FG][BG] row that every
  color-using tool renders (brush, bucket, eyedropper).
- The eyedropper previously inferred its target swatch from
  active_color_mode, which was only ever set as a side effect of opening
  a color picker — so where a sample landed depended on invisible state.
  It now has an explicit toggle, and active_color_mode is gone.
- Effect brushes (dodge/burn, sponge, blur, smudge, clone, heal, pattern)
  were stuck on a hardcoded Gaussian dab. Replaced ~20 flat per-tool
  fields with a BrushKind -> BrushSlot model: every dab-painting tool now
  owns its size/strength/hardness/spacing, its .myb preset, and its FG/BG
  choice, and gets the full brush library. base_settings already flowed
  into the dab engine independently of blend_mode, so this needed no core
  or WGSL changes.

Cursors
- Rebuilt around three kinds instead of "icon = cursor": System (real OS
  cursor for Select/Transform), Precise (glyph with a real tip, e.g.
  pencil/pipette), and Badge (crosshair marks the click point, glyph sits
  to its bottom-right) for icons with no focus point.
- Cursors are now OS-composited via egui::CursorImage rather than painted
  into the scene, so they no longer lag the pointer. Glyphs are rasterized
  from egui's font atlas using epaint's own placement maths, so they land
  where painter.text put them. Drops the SVG cursor rasterization path.

Tablet
- The Wayland backend never handled zwp_tablet_tool_v2 Button events, so
  stylus barrel buttons were silently dropped. Both Wayland and X11 now
  emit them, with actions bound in Preferences (Pan/Eyedropper/Eraser).
  A pan-bound button suppresses the tool the way Alt+drag does, since the
  pen tip is still down while panning. Also adds middle-mouse panning.

Timeline
- Raster layers had no opacity control; the slider always drove volume,
  which is meaningless on them. The layer row is now three tiers, and each
  layer only shows controls that apply to it: Vector/Video get both volume
  and opacity (they can hold movie clips with audio), Raster/Text get
  opacity, Audio gets volume. Mute/solo are hidden where there's no audio.
- Layer toggles use Lucide icons with tooltips, and gain a visibility
  (eye) toggle. LayerProperty::Visible already existed but was only
  reachable from the info panel.
- Below 1.5x the layer-header width, the track area is dropped and the
  headers take the full pane.

Panes
- Color picker popups closed on any click (including on their own hue
  slider) and would not close on a drag — so drawing on the stage left one
  open. Shared color_swatch widget closes on a press outside instead.
- Stage header gains undo/redo buttons for tablet use, and taller targets.
- Toolbar is real egui layout (horizontal_wrapped) instead of absolute
  rect math, so it scrolls when the pane is too short.
- Tools with no SVG icon use Lucide glyphs instead of a TODO placeholder.
2026-07-14 13:04:38 -04:00
Skyler Lehmkuhl c62164c365 Cycle recording: MIDI separate-takes mode, and append to existing folders
Completes the cycle-recording spec. Three related pieces:

MIDI separate takes (Preferences > Audio > "Cycle MIDI recording"):
- Each pass becomes its own MIDI clip, folded into a take folder — the same
  shape audio always gets — instead of merging into one clip. Merge stays
  the default.
- Notes are bucketed by the pass they were played in. The pass counter bumps
  BETWEEN close_active_notes and the re-note_on at a wrap, so a key held
  across the boundary has its sounding half filed under the pass that's
  ending and its re-opened half under the pass that's beginning. Put the bump
  on either side of that pair and the whole note lands in one pass; there's a
  test named for exactly that.
- A silent INTERIOR pass still yields an empty take, so take N is always pass
  N — otherwise the numbering silently shifts and "take 3" stops meaning "the
  third time round". A TRAILING empty pass is dropped: that's what hitting
  stop shortly after a wrap gives you, a stop artifact rather than a take you
  played. (Audio already behaved this way via its short-final-take rule.)
- Still triggers on the wrap: stop inside the first pass and it's an ordinary
  single recording, whatever the preference says.

Append to an existing take folder (AppendTakesAction):
- Cycle-recording over a region that already holds a take folder now ADDS to
  that folder rather than dropping a second clip on top of it, which stranded
  the new takes in an overlapping clip you couldn't audition against the old.
- "Same region" means same start AND same loop length: resize the cycle region
  and you get a fresh folder, rather than takes of a different length appended
  to an existing one, which would break the uniform-take invariant that
  comping-via-split depends on.
- The recording's own clip/instance are throwaway scaffolding here (the takes
  already live in the backend pools), so they're discarded; the single
  AppendTakesAction is the whole undoable step.

Don't play the region you're recording over:
- While recording into a MIDI track, every clip on that track is silenced
  EXCEPT the one being recorded into. A take folder already sitting in the
  cycle region was otherwise playing its active take underneath you on every
  pass, fighting the part you were trying to record.
- The recording clip itself is exempt, because in merge mode that's precisely
  what you want to hear: the overdub you've been building up. Other tracks are
  untouched.
2026-07-14 12:10:08 -04:00
Skyler Lehmkuhl a2839f80b1 Clean up build warnings 2026-07-09 13:24:32 -04:00
Skyler Lehmkuhl f13a127c9d Fit modal dialogs to the phone screen on mobile
Add mobile::dialog_width(ctx, desired) — a min()-clamp that's a no-op on wide
desktop screens. Clamp the Export and Export Progress modals and the Sample
Import window to it. Render Preferences as an egui::Modal (dim backdrop, centered,
no title bar) with a screen-fit scroll height when mobile_active(); desktop keeps
its window. Extracted the preferences body into render_body() shared by both paths.
2026-07-01 06:28:04 -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 1cc7029321 make keyboard shortcuts configurable 2026-02-25 07:36:53 -05:00
Skyler Lehmkuhl 6fcee92d59 Add user preference to show waveforms as stacked stereo 2026-02-15 04:50:33 -05:00
Skyler Lehmkuhl 777d3ef6be Clean up build warnings 2026-02-14 11:07:32 -05:00
Skyler Lehmkuhl caba4305d8 Allow setting node cv inputs via slider, add preferences window 2025-12-17 07:38:10 -05:00