schedule_notes ran format!() every block for every instrument track; scan
the edit values by prefix instead.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
clip.unloop found loop copies only by comparing content of back-to-back
clips. As soon as the source clip's notes were edited, its content no
longer matched the copies and unloop answered "the selection is not a
looped clip" for the very loop that clip.loop had just created.
clip.loop now tags the origin and its copies with a shared `loop_id`
(new optional Clip field, serde default so old sessions load), and
unloop treats equal-length adjacent clips with the same loop_id as one
loop. The content comparison stays as a fallback for loops without a
tag. unloop with mode=flatten clears the tag so the copies become
ordinary independent clips.
Propagating edits from the source to the copies is not part of this
change.
Plugin::set_param only queues the value for the processor, so a parameter that lives in the controller alone (no processor state) was saved with the controller's old value and came back wrong after load_state. set_param may run on the audio thread, so it now just marks the value; save_state, which already calls into the controller, pushes the marked values with setParamNormalized before taking the snapshot.
A MIDI clip whose start was trimmed may keep the events before its new
start at negative ticks, hidden until the start is trimmed out again
(midi-clip-trim does this). The controller loop clamped every event's
tick to 0, so a hidden sustain pedal or pitch bend played at the clip
start, and the chase after a seek counted hidden values as the state
in effect.
Both loops now skip events before tick 0. Nothing changes for clips
without hidden events: notes and controllers made by the app never
start before tick 0.
Test: a sustain-pedal event at tick -480 is neither sent when the clip
starts nor chased after a seek into the clip, while a visible
controller is still chased.
A narrow Mix strip's sections are 56 pt wide, and INSERTS A-E (63 pt),
INSERTS F-J, SENDS A-E, SENDS F-J and INSTRUMENT (67 pt) ran into the
neighbouring strips. Pro Tools' Narrow Mix shortens them instead.
- A section name that doesn't fit with 4 pt to spare on each side uses
Pro Tools' short form: I A-E, I F-J, S A-E, S F-J, and INST.
- The group selector says "nogrp", as Pro Tools' does, instead of
"no gr…".
- Narrow strips leave the empty comments box without its hint, which
wrapped as "comme / nts".
Normal strips are unchanged. tests/mix_narrow_headers.rs checks both
widths, including that no label in a narrow strip is cut short; it
fails on main.
Pro Tools draws a strip's fader and meter as one unit: the meter's dB
scale sits between them, the meter spans the fader's travel, and its
bar is a solid column over dark tints of its zones. Ours had LED
stripes, no meter scale, and a meter taller than the fader's travel.
- Meters: a solid bar that lights green, yellow and red over dark tints
of those zones (fainter in light wells), ruled at the scale's marks,
with a clip light above the bar and a 2 pt peak hold in its zone's
colour. This is the shared meter, so the Edit window, toolbar and
transport meters change too.
- Mix strips: the meter runs the fader's travel, 0 dBFS level with
+12, and its 0-60 dB scale sits between the fader and the meter.
Narrow strips show the fader's ticks without numbers, as Pro Tools'
Narrow Mix does.
- Faders: a narrower, taller cap (18 x 40 pt, 16 pt wide in narrow
strips) with a pinched waist and a dark centre line, in a 4 pt slot.
The scale's numbers sit beside the cap and skip marks too close to
the one above.
tests/mix_strip_layout.rs checks that the meter scale's 0 is level
with the fader's 12 and that narrow strips draw no scale numbers; both
fail before this change.
Part of #138.
detect_transients (Tab to Transient, Separate at Transients, Beat Detective)
missed most onsets that weren't the loudest around, and put notes that start
over a ringing note 25-35 ms early, in the previous note's tail. Its
threshold was a share of the loudest flux in the analysed audio, so quiet
hits vanished next to loud ones and the result changed with where the
analysis started. It placed each onset on the first sample reaching a
quarter of the frame's peak, which a ringing note already does.
- Vibrato-suppressed spectral flux: 21 ms windows 5.3 ms apart, log
magnitudes on 24 bands per octave from 30 Hz to 20 kHz, each band compared
with the loudest of its neighbours in the frame before, so vibrato and
chorus don't count. Channels are analysed as the sum of their energies,
so stereo content that cancels in mono still counts.
- A peak must be the highest within 30 ms and exceed the mean flux from
100 ms before to 70 ms after by a fixed amount (2.5 at sensitivity 0.5),
so quiet hits count and the result doesn't depend on where the audio
starts.
- A fast release smears energy between a note's partials, which looks like
a rise. A detection also needs a 3 dB step in its high-passed energy, or
total energy that grows in its frame or the next.
- Each onset goes to the step edge of the high-passed energy (the sample
where the next 3 ms gain most over the 6 ms before), then back to where
the energy crosses the midpoint between the two: 0.3-0.5 ms before
percussive attacks.
On 29 synthetic cases with known onsets (drums with ghost notes; drum-synth
grooves dry and through reverb, compression and limiting; 180 bpm; flams and
claps; legato piano; plucked bass and guitar; strums; 808 kicks; hand drums;
synth bass, leads, pads and staccato notes; string swells; a vibrato pad;
quiet hits over noise and hum; stereo pairs; formant syllables), the
F-measure went from 0.658 to 0.977. It found 1398 onsets instead of 870,
with 5 false ones instead of 315; all 5 are vowels starting after a
consonant. Still missed: hats under a crash, ghost notes under a louder
hit's tail, and slow string swells.
The old offline_meters test bursts stopped dead at -37 dB, an audible click
that now counts as a transient; they ring out instead.
Tab found the next transient only on the first selected track, analysed
everything from the cursor to the end of the track on every press, and
Alt+Tab, Shift+Tab and Alt+Shift+Tab ignored Tab to Transient (Shift+Tab
did nothing at all). The toolbar button used the Selector tool's icon.
- A new command, edit.tab {back?, extend?}, does what the Tab keys do, as in
Pro Tools: Tab and Alt+Tab (Option+Tab on a Mac, Ctrl+Tab on Windows and
Linux) move the insertion point to the next or previous clip start, end or
sync point on the selected tracks; Shift+Tab and Alt+Shift+Tab
(Ctrl+Shift+Tab) extend the selection. Alt+Shift+Tab pulls the selection
end back while a stop lies inside the selection, then grows its start.
- With Tab to Transient on, the stops also include the transients on every
selected audio track and the note-ons and note-offs on MIDI and Instrument
tracks. A transient within 10 ms of a clip boundary or note is that stop,
not a second one.
- Transients are detected in fixed 10-second blocks of each clip, so Tab and
Alt+Tab always find the same ones and a long clip costs one block per
press. Analysed blocks are cached while their clips, clip effects and audio
are unchanged: 0.5 ms a press across the demo's five audio tracks, 43 ms
without the cache.
- The Tab to Transient button gets its own icon, an arrow into a transient.
Cmd+Alt+Tab still toggles Tab to Transient. On Windows and Linux the desktop
takes Ctrl+Alt+Tab, and Pro Tools' Ctrl+Alt+T is Thin Automation here too, so
the button and the Options menu are the way there.
A bounce keeps its own snapshot of the session, decoded audio included.
The progress window blocks the mouse, but shortcuts, the macOS menu bar,
dropped files and the control channel still reach SoundApp::run, so a
second session could be opened beside the one bouncing; two large ones
could exhaust memory, which aborts the app.
New, Open, Close and Revert now fail with "Finish or cancel the bounce
first" while a bounce runs, and so do sessions opened or dropped in the
browser.
CLAP and VST3 plugins report their latency unchecked (up to u32::MAX
samples), and an offline render skips that many samples before its first
block, inside one call. A plugin reporting garbage made a bounce render
about 25 hours of audio before producing anything, with the UI stuck in
that call: no progress, no Cancel. Freeze and Commit stalled the same
way.
- A strip reports at most 2^20 samples of latency (about 22 s at 48 kHz),
the most delay compensation could align anyway (MAX_LATENCY, shared
with the delay lines). That holds for the latency an aux's inserts
report before the aux is processed, too.
- RangeRender::next_block renders one block per call, returning no frames
while it skips the latency, so a bounce yields to the UI there too.
Bounce Mix rendered the whole file inside one UI frame: the window stopped
drawing and nothing could cancel the bounce until the file was written,
which takes minutes for long or plugin-heavy sessions.
- engine: io::Bounce renders a slice at a time (step) and reports its
progress; dropping an unfinished one cancels it, removing the partial
file and keeping any older one. cmd::bounce_job builds it from
file.bounce_mix's params; the command and stems still run it to
completion.
- ui: the Bounce dialog starts the job and logic() renders it for 30 ms
per call (32 blocks in the browser, where Instant panics), requesting
another call each time, so it carries on while the window is hidden
(eframe then runs logic alone). A modal shows the file, a progress bar
and Cancel (or Esc); the status line reports the result, and the Task
Manager lists the running bounce.
- It stays on the UI thread because hosted plugins must be created there.
A bounce rendered the whole mix (or bus) into memory and then encoded a
second copy, and the render silently stopped at 2^29 samples: about 93
minutes of stereo at 48 kHz, 31 of 5.1, 15 of 7.1.4. Longer bounces came
out cut short while file.bounce_mix still reported the requested length.
- audio-io: FileEncoder writes WAV/BWF (RF64 above 4 GiB), AIFF and FLAC
block by block into any Write + Seek, byte for byte what encode writes
for the whole buffer. check_room fails up front when an AIFF (4 GiB) or
a FLAC (2^36 frames) can't hold the bounce. AIFF and FLAC encode run on
the same writers; their output is unchanged.
- mix: RangeRender renders the main mix or one bus over a range of any
length, a block at a time; render_range and render_bus (in memory,
still capped) are built on it.
- engine: io::Bounce streams the main mix or a bus, or its stereo
fold-down, into "<name>.part" next to the target, syncs it and renames
it into place, so a failed bounce leaves no partial file and keeps the
old one. In the browser it encodes into memory (failing instead of
aborting when memory runs out) and delivers the file as before.
Normalize renders twice (peak, then the scaled write); levels are
measured as blocks are written. Stems stream the same way and share the
4-hour limit.
A 100-minute bounce of the demo session now writes all 288,000,000
frames with 83 MB peak memory.
Pro Tools draws a take's waveform as it records; ours showed nothing
until Stop. Each record-enabled track now shows its take growing as a
red clip with its waveform, drawn from the take's live summary through
the same column grid and channel bands as clips (Peak view, as Pro Tools
always shows while recording).
- The take writer keeps, per take channel, the (min, max) of every 128
frames it writes (at most 2^22 blocks, three hours at 48 kHz; the file
records on regardless). Recorder::live_take hands the Edit window what
a track's take holds so far, while the writer runs. The blocks sit
behind a mutex the writer thread and the UI lock briefly; the input
callback is untouched.
- io::live_span places the take as Stop will: from the record start,
only the pass under way when loop recording, and within the punch
range.
Tests: the take writer's summary (record.rs), live_span with loop and
punch (io.rs) and draw_live (tests/waveform.rs).
Double-clicking a MIDI clip in the Edit window only selected it, so the
MIDI Editor had to be opened from Window › MIDI Editor or the dock tabs.
In Pro Tools a Grabber double-click on a MIDI clip opens it in a MIDI
Editor (the "Double-Clicking a MIDI Clip Opens" MIDI preference, which
defaults to an editor); SoundCraft has one, docked, editor.
- A double-click with the Grabber on a MIDI or instrument clip selects
the clip, as before, and opens the docked MIDI Editor on it. An editor
that's already open stays open and switches to the clip.
- The Smart Tool follows its zones: its lower half is the Grabber and
opens the editor; its upper half is the Selector, where a double-click
still only selects the clip, as in Pro Tools.
- Audio clips are unchanged: a double-click selects them.
Tests: a Grabber double-click opens the editor on the clip; with the
Smart Tool, the upper half selects without opening it and the lower half
opens it; an audio clip leaves the editor closed. The first two fail on
main.
A device or driver that sends NaN or infinite samples got them written
into the take, where they poison whatever the clip is mixed with, and
into the input monitor. Both paths now turn them into silence before
they are queued, so a take keeps its length and timing.
Takes stream to disk while recording, but they only join the session at
Stop. After a crash the files were on disk with nothing pointing at
them and no record of where they started, and the header of each was up
to a second behind its audio.
When recording starts, its plan (each track's take file, where the
takes start, the loop length and punch range) is autosaved with the
session, and every autosave until Stop keeps it there. Stop places the
takes from that same plan. Recovering a folder that still holds a plan
first repairs each take's header to cover all of its audio, then places
the takes as Stop would have, skipping any the session already has. The
recovery window says when there's an unfinished recording, and such a
folder is kept even if its session file was saved later.
Opening, creating, closing or reverting a session in the middle of a
take dropped the recorder: the take was written to disk but never added
to any session. Quitting mid-take did the same, and on_exit didn't
finish the take either.
New, Open, Close, Revert and Quit now wait for the take: the engine
refuses them with "stop recording first", and their menu items are
greyed out while recording, as is Recover in the recovery window. Any
exit that still happens mid-take (closing the window, the system
quitting the app, a remote quit) finishes the take first and keeps the
session, take included, in the autosave folder for the next launch.
Losing the output device only reached the log: the transport kept
"playing" a frozen playhead, and a recording ran on with nobody told.
The output now reports trouble once: the device was disconnected, or it
stopped asking for audio for two seconds while playing (not checked in
the web build, which has no clock for it), or the engine hit a bug as
before. The app stops the transport, keeps a take in progress, and says
what happened and how to reconnect.
A failing input only reached the log. When the interface was unplugged
mid-take (on macOS its callbacks simply stop), when writing a take
failed (a full disk), or when the input callback panicked, recording
went on with the transport running and the take silently ended early.
A panic in the callback could also take the input stream down for good.
The recorder now reports trouble: the device was disconnected, it sent
nothing for two seconds while recording, its callback hit a bug (caught,
after which it does nothing more), or writing a take failed. The app
stops recording at once, keeps the take up to that point and says what
happened and what to do. Input lost to a slow disk is reported while
recording instead of only at Stop. Reconnect audio device now reopens
the input as well as the output, so monitoring works again afterwards.
Autosave ran every two minutes and not at all while recording, so a
crash could lose minutes of work. It also wrote on the UI thread: the
first autosave after a recording copied the take, which could freeze
the window for seconds.
The open session now goes to a writer thread after every change,
recording or not. The first change after a pause is written at once;
while changes keep coming, as in a fader drag, the newest is written at
most once a second. The UI waits for the writer only where it needs the
result: after a caught panic, when recovering and when quitting. A bug
in a write is caught and reported, and the next change tries again.
Plugins' own state still reaches the session every two minutes and not
while recording, since plugins save it on the audio thread.
Nothing caught a panic in the audio output callback. On macOS it
unwinds into CoreAudio's thread, which most likely aborts the app; on
Windows and Linux the stream thread dies while the transport keeps
showing playback with no sound. A panic in the silent clock that runs
without an audio device stopped the clock for good.
Each block is now filled under a guard. A panic makes that block and
every later one silent and marks the engine failed. The app then stops
the transport, keeping what was recorded, and says to reconnect the
audio device or restart.
Nothing caught a panic outside a command: one in the interface's frame
code ended the process in that frame (exit code 101), losing every
unsaved change, and the log got a single line with no backtrace.
Each frame's logic and drawing now run guarded. A panic there autosaves
the session (once per change, so a bug that repeats every frame doesn't
flood the disk), says so in the status line and lets the app carry on.
If the app goes down after all, the autosave is offered at the next
launch. The panic hook now logs a backtrace.
Autosave wrote every session called "Untitled" to the same
Autosave/Untitled folder and reused audio files there by source id, so
a second untitled session, or a take recorded after an undo, could be
autosaved with another take's audio. Its first run also copied all of
the session's audio again. Nothing ever offered an autosave back
(latest_autosave had no callers), so after a crash the work could only
be found by digging in the settings folder.
Each open session now autosaves to a folder of its own, locked while
the session is open. Audio already saved with the session is referenced
where it is; audio that exists only in memory is copied once per
recording or render. Saving, closing the session, opening another and
quitting with SoundCraft's Quit delete the folder. A crash, or an exit
that skips Quit with unsaved changes, leaves it behind, after one last
autosave when SoundCraft gets the chance. At the next launch,
SoundCraft offers each kept folder, newest first, to recover or
discard. Recovered work opens as an unsaved version of the session it
came from, so Save writes it back there; unsaved changes in the session
it replaces are kept the same way. Folders older than their session
file are dropped.
Pro Tools has File › Open Recent; SoundCraft had no way back to a session but
browsing for it. The list (UiState.recent_sessions) records the engine's
session path whenever it changes, so opening or saving by any route (menu,
control channel, MCP, a dropped file) is covered by one rule.
- Under File › Open; each entry is "name — folder", full path on hover.
- A session that has moved or been deleted is greyed out "(missing)" instead
of failing on click; existence is re-checked at most every 3 s while the menu
is open, since a network share costs a round trip per file.
- Opening goes through the dialog layer, so unsaved changes are asked about.
- Clear Recent empties it.
- The desktop host saves ui.json as soon as the list changes, not only on a
clean exit: the list matters most after a crash.
- An older ui.json without the field still loads (serde default).
The macOS system menu bar (native_menu) does not show it yet; the egui menu
bar on Windows and Linux does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With no widget focused, egui answers Tab by giving keyboard focus to the
first widget in the window, and Shift+Tab to the last one. shortcuts::handle
ignores every key while a widget has focus, so after Tab (to the next clip
boundary) R/T zoom and every other shortcut did nothing until a click
elsewhere or Escape. Space and Enter clicked the focused widget instead: on
macOS that is the toolbar's Shuffle button, so Space switched the session to
Shuffle; on Windows and Linux it opened the SoundCraft menu.
While the shortcuts own the keyboard (nothing focused, no dialog open),
handle() now cancels egui's focus move for the frame. Tab still moves between
fields once one has focus, and inside dialogs.
set_meter and remove_meter now re-place every later meter event at the
bar number it started before the edit, so meter changes stay on bar
lines and bar|beat positions round-trip. Clips, notes and tempo events
keep their positions.
Fixes#250
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The shelf corner (~1682 Hz) sits at or above Nyquist for rates under
~3.4 kHz. The bilinear prewarp tan() then wraps negative (unstable
poles, +750 LUFS readings, -144 integrated) or aliases. Hold the
design frequency below Nyquist (0.49 * sr, as Coeffs::design does).
Coefficients at 8 kHz and above are unchanged.
Fixes#240
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dragging a track's bottom edge showed nothing until release; then the
track jumped to the nearest height preset. Pro Tools' track height
modifiers and keys were missing too.
Dragging:
- The edge follows the pointer as in Pro Tools: the track steps through
Micro…Extreme while the pointer moves, and the tracks below move with
it. The whole drag is one undo step (execute_merged, ended on release
like fader moves).
- Cmd (Ctrl on Windows and Linux) drags to any height from Micro to
Extreme instead of snapping. The model gains TrackHeight::Custom
(points), and track.height takes a number of points as well as a
preset name.
- Alt resizes every track the Edit window shows; Alt+Shift resizes the
selected tracks too. The track menu's Track Height items follow the
same rule.
- The drag is measured from where it started, not from the edge's
current place: when the tracks above the dragged one resize they move
its edge, and edge-relative math flipped between two heights on every
frame. This also fixes rows showing playlist lanes, which a plain
click on the edge used to make taller.
Keys:
- New commands track.height_up and track.height_down move each track
one preset taller or shorter; a track already at Extreme or Micro is
left alone, with no undo step. Control+Up/Down runs them on macOS, as
in Pro Tools. Windows and Linux use Ctrl+Up/Down, because the Start
key Pro Tools uses there isn't available. Alt steps every shown track,
Alt+Shift the selected ones.
- Ctrl+Up/Down used to run Extend Edit Up/Down, keys Pro Tools doesn't
use for them. Those commands now have Pro Tools' Control+Shift+P and
Control+Shift+; on macOS (macOS sends the latter as Control+:).
Windows and Linux reach them from Edit > Selection.
- Off macOS, menus no longer show Mac Control chords that can't be
pressed there (Time Lock's Ctrl+Alt+L, Toggle Recent Playlist's
Ctrl+Alt+Shift+Left, the new Extend Edit keys); the arrow chords that
fixed() maps to Ctrl keep their labels.
Track headers show each row of controls only once it fits: the keys
from 46 points and the view and automation selectors from 86, where
each row ends. The old 30 and 60 point cutoffs suited the presets only
and cut controls off at heights in between. Every preset draws exactly
as before.
Tests: the model's custom heights, steps and JSON; the engine's point
heights and steps, with no undo step when nothing changes; in the UI,
the live drag and its single undo step, Cmd, Alt and Alt+Shift drags
that hold steady while the pointer does, the height keys on macOS,
Windows and Linux, and the Extend Edit keys on macOS.
No change in behaviour or look: the Edit and Mix windows render
pixel-identical in both themes.
- The Mix strip and the Edit window's I/O column share input_name
(In 1 for a recording track with no input) and route_popup, which
keeps the route menu open while its bus search is typed in.
- The Mix slots and the I/O column's output key share widgets::groove;
the Edit header meter and widgets::meter share level_pos.
- The column list is read once per frame and passed to each track row;
header_width takes it.
- The record, input, solo and mute keys are a table, the slot columns
one loop, and send marks a char instead of a String.
- Inserts look their plugin up once; the unused triangle_right icon is
gone.
In Pro Tools a send slot's name picks its destination, and only the
small fader icon at its right opens the send's level. Here the level
control covered the slot's right 42 points, more than half of an Edit
window cell, while the icon is 5 points wide. It now covers the icon and
3 points either side.
The icon does what the whole level area did: a click opens the Send
window, Alt-click sets 0 dB, right-click opens the send's options and a
drag sets the level. A short track's square has no icon, so all of it
stays the level control.
Measured from the user's Pro Tools screenshots:
- Track headers are 136 points wide (were 236). A 14-point colour strip
holds an options button (a folder's open/close) and, at the bottom, a
key that shows or hides volume automation. The name is bold beside a
playlist menu and highlighted when the track is selected (the blue
outline that marked a selected tinted header is gone). Then come
16-point record, input, solo and mute keys (record stays dark red when
off, and a mute implied by another track's solo shows dark orange);
the track view; freeze, voice (dyn, or off to make the track
inactive) and automation mode; timebase and the Elastic Audio
algorithm (an instrument track's instrument); and a slim meter down
the right edge. Rows drop away on shorter tracks.
- The vol/pan and inserts summaries and the AUX/INST labels are gone:
the I/O and insert columns show them.
- The I/O column has input and output selectors, a fader key that shows
the track in the Mix window, and a black vol/pan readout.
- Insert, send and instrument columns are 75 points wide on a neutral
well, with 15-point cells every 18 points. An empty insert shows a
dot, an empty send its letter. Tracks too short for three cells show
a row of squares, with arrows for sends.
- A used slot is a rounded, raised grey button: its dot or letter, a
groove, the name centred in 9.5-point bold, and for sends a small
level box (the level in dB is in the tooltip; the box still opens the
Send window or drags the level). A bypassed insert turns blue. The Mix
window's slots share this drawing; filled ones don't brighten on
hover, which would cost the name its contrast.
- New slot_well, slot_empty, slot_face, slot_bypassed and slot_mark
colours replace insert_on, insert_bypass, send_on and slot_border.
mix_instrument_contrast now reads the slot's painted gradient, and
finds the empty instrument slot by its cell, which shows a dot
rather than "no instrument".
- The header's record, input, solo and mute keys carry accessibility
labels.
Part of #138.
The edit modes only partly worked. Shuffle changed Cut, Clear and Paste
but not moves or trims. Spot froze drags and had no dialog. Grid snapped
relative to whichever clip was under the pointer, and Relative Grid
acted like Absolute. Behaviour now follows the Edit Modes chapter of the
Pro Tools Reference Guide.
Shuffle
- Grabber drags lift the clips out (later clips close the gap) and land
them at the nearest clip boundary, pushing later clips right; an empty
track takes them at the session start. The drag outline shows where
each track's clips will land.
- Trims move the later clips (a trimmed start stays put and the material
slides under it). Duplicate, Duplicate and Extend, Repeat and Clip List
drops make room, and Paste over a selection replaces it.
- Moves lift each clip by id and close the hole it leaves, so crossfade
partners and unselected clips keep their audio, and a crossfaded pair
moved together stays a crossfade. Ripples never push clips past the
session start, never split a crossfade (refused with a reason), and
drops never land inside a clip or crossfade.
- A time- or edit-locked clip and everything after it stay put when
Shuffle closes or opens gaps. Edits that would push into one are
refused ("not enough room before a locked clip"), and trims stop short
of it. Insert Time and Cut Time are timeline operations and still move
everything.
- With Automation Follows Edit, automation moves with every ripple, up to
a locked clip, and travels with moved clips; with it off, Shuffle edits
leave automation alone. Shuffle Clear used to move the clips but leave
their automation behind (#232).
- Shuffle Lock: Cmd-click (Ctrl-click on Windows and Linux) the Shuffle
button while in another mode. While it's on, the button shows a padlock
and every way into Shuffle (keys, button, commands) is refused.
- A drag works out every lift and landing point once per target track
(`lift_plan`, `shuffle_slots`, and a `DragPlan` kept for the drag). In a
release build, dragging 100 clips on a track of 20,000 took 29 ms a
frame and a 135 ms drop; now it's under a microsecond a frame and a 5 ms
drop.
Slip, Spot and Grid
- Slip: trims stop at the neighbouring clips instead of overlapping them.
- Spot: a Grabber click or drag, a Trim click or drag and a Clip List drop
open a Spot dialog: Start, Sync Point and End in any time scale, the
Duration, and the clip's Original Time Stamp. Trim clicks pick the
nearer edge.
- Grid: a drag snaps the dragged clip's start (its sync point, when it
has one inside the clip) to the grid. Relative Grid moves by whole grid
steps and keeps the clip's offset from the grid; a cut or copy pasted
in Relative Grid lands as far after a grid line as the copied range
started after one. Holding Cmd (Ctrl on Windows and Linux) while
dragging or dropping from the Clip List swaps Grid and Slip. Grid lines
no longer fall a step short at tempos where a line isn't a whole number
of samples.
- Choosing Grid from another mode picks whichever of Absolute and
Relative Grid was used last; F4 in Grid toggles them, like clicking the
Grid button again. Options > Edit/Tool Mode Keyboard Lock, which did
nothing, now stops F4 and Alt+4 from switching them (toolbar clicks
still do).
- Snap to Grid in Shuffle, Slip and Spot: Shift+F4, Shift-click on the
Grid button or on another mode's button, or hold F4 with F1-F3. The
Edit cursor and selections snap to the grid while clip moves and trims
follow the main mode; the Grid button is outlined while it's on.
- Option+1-4 (Alt+1-4 on Windows and Linux) work like F1-F4, and the `
key cycles Shuffle, Slip, Spot and Grid, skipping a locked Shuffle.
- Insert Silence pushes later material only in Shuffle; in the other
modes it clears the selection, as in Pro Tools.
Also
- Dragging clips from several tracks keeps each track's distance from the
grabbed clip's track. Drops past the last track, or onto a track that
can't play the clips (audio onto MIDI, an aux or the master), are
refused; clips moved onto an aux used to vanish into its unused
playlist.
- Undo and Redo keep the Edit mode, Shuffle Lock, Snap to Grid and the
Grid kind. Start trims keep a clip's sync point on its audio, or drop
it. Imported BWF time stamps are converted from the file's rate (read
from its header) to the session's, and the Original Time Stamp no
longer adds the session start twice. Typed positions that overflow are
ignored instead of panicking.
Commands: `edit.move_clips` follows the mode, and its `track` moves the
first clip's track there and keeps the others' distances. `edit.trim_clip`,
`edit.snap_to_grid` (Shift+F4) and `edit.shuffle_lock` are new, and
`edit.mode` takes `snap_to_grid`. Mode buttons expose their names to
accessibility tools.
Tests: crates/{engine,ui-egui}/tests/edit_modes.rs and the op tests in
edit.rs; the random-edits property test also toggles clip locks.
midi.new_clip had no UI path, so an empty instrument/MIDI track could
never get a MIDI clip (#282). The menu bar is built from the catalog,
so the entry is added to menus::tree() and placed first in Clip.
The new test checks every mutating command is reachable from the GUI
(built menu tree, shortcut, or UI reference), with today's gaps listed.
Closes#282
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>