Clips: Stop AudioClipBase's constructor making undoable writes - #433
Conversation
#415) A clip can be constructed whilst an undo is in progress - undoing a record or a clip deletion re-adds the clip's state, which rebuilds the Clip from valueTreeChildAdded inside the undo transaction. Any write made through the Edit's UndoManager at that point re-enters UndoManager::perform, which refuses it, asserts, and silently discards the change. CachedValue still updates its cached copy, so the object and the state then disagree until the Edit is reloaded. Made the constructor's corrections and migrations non-undoable: - checkFadeLengthsForOverrun() now clamps fadeIn/fadeOut with a null UndoManager; it's derived geometry, not user data, and setFadeIn/setFadeOut still do their own clamping on the user-facing paths - the pan limit, the legacy timeStretch migration and TimeStretcher::checkModeIsAvailable() likewise - the LOOPINFO child is created with a null UndoManager, so a refused perform() can't leave LoopInfo referring to a detached tree Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## develop #433 +/- ##
===========================================
+ Coverage 59.81% 59.83% +0.02%
===========================================
Files 568 568
Lines 79964 80002 +38
Branches 12381 12382 +1
===========================================
+ Hits 47828 47869 +41
+ Misses 32136 32133 -3 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Will this have any problems when undoing? For example, if a clip gets copy/pasted then undone then redone, will these properties get lost? Or will they just get reset again to the defaults in a non-undoable way? |
|
🤖 Good question - I tested that exact scenario rather than reasoning about it. The properties survive; they don't get lost, and they don't need re-deriving. Paste → undo → redo, with the issue's fixture (length Two independent reasons it holds:
So it's the second half of your alternative: they'd get reset again in a non-undoable way - except they don't even need to be, because they were never removed. Worth being straight about the one case that is imperfect, though it's unchanged by this PR: nothing re-clamps the fades when a clip is trimmed shorter ( Happy to add the paste/undo/redo round trip as a third |
Summary
AudioClipBase's constructor wrote to the clip state through the Edit'sUndoManager. A clip can be constructed whilst an undo is in progress, so those writes were being silently discarded. This makes the constructor's corrections and migrations non-undoable.Root cause
Undoing a record, or undoing a clip deletion, re-adds the clip's
ValueTreechild inside the undo transaction.valueTreeChildAddedrebuilds theClip, and the constructor'scheckFadeLengthsForOverrun()then rescalesfadeIn/fadeOutthrough aCachedValuebound toedit.getUndoManager().That re-enters
UndoManager::perform, which refuses to run recursively - it hitsjassert (! isPerformingUndoRedo())atjuce_UndoManager.cpp:128and drops the action. The correction never reaches the state.Worse,
CachedValue::setValueassigns its cached copy before callingValueTree::setProperty, so after a refused perform the clip object reports the corrected fade whilst the state still holds the overrunning one. The two only diverge until the Edit is next loaded, at which point the bad fade comes back.Fixed by making everything the constructor writes non-undoable:
checkFadeLengthsForOverrun()clampsfadeIn/fadeOutwith a nullUndoManager. This is derived geometry, not user data, and the user-facing paths (setFadeIn/setFadeOut) do their own clamping undoably as before.panlimit, the legacytimeStretch->stretchModemigration andTimeStretcher::checkModeIsAvailable()likewise - all are corrections/migrations rather than edits.LOOPINFOchild is created with a nullUndoManager.ValueTree::getOrCreateChildWithNamereturns a detached tree when theappendChildperform is refused, which would have leftLoopInfowriting into a tree that isn't in the Edit.A comment on the constructor records the constraint so it doesn't regress.
Regression test
"Fade length correction isn't undoable"intracktion_Clip.cpp, using the issue's fixture - a clip of length0.00999999999999979carrying a0.02fade-out:fadeOutmust survive. Fails on develop (0.02vsApprox(0.01)).UndoManager::perform. Asserts on thefadeIn/fadeOutproperties in the state, not theCachedValueaccessors, which is what actually broke; also checks the undo history is still intact afterwards. Fails on develop (0.02 <= 0.01).Both subcases fail on develop and pass with the fix.
Testing
Full TestRunner suite (Release, macOS): 370/370 doctest cases, 21267 assertions, green.
Fixes #415
🤖 Generated with Claude Code