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.
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.
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.
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.
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.