Commit Graph
630 Commits
Author SHA1 Message Date
Brandon Thomas 8c33cefe1c Merge pull request #286 from marcoarnulfo/fix/modulation-feedback-tail
fix(dsp): chorus and flanger tails follow their feedback (#242)
2026-10-13 04:27:23 -04:00
Brandon Thomas 5bb3e7b3b6 Merge pull request #285 from marcoarnulfo/fix/pitch-shift-full-range
fix(dsp): pitch_shift reaches ±48 semitones (#243)
2026-10-13 04:27:18 -04:00
Brandon Thomas f58da8fe8a Merge pull request #284 from marcoarnulfo/fix/elastic-properties-ratio
fix(clip): Elastic Properties stretches the clip by the requested ratio (#227)
2026-10-13 04:27:14 -04:00
Brandon Thomas 99dbd4d628 Merge pull request #283 from marcoarnulfo/fix/marker-edit-name
fix(markers): markers.edit applies the documented name (#236)
2026-10-13 04:27:10 -04:00
Brandon Thomas 741010d50c Merge pull request #281 from marcoarnulfo/fix/h264-param-set-overflow
fix(video): H.264 parameter sets check their arithmetic before range checks (#251, #252)
2026-10-13 04:27:06 -04:00
Brandon Thomas 3726c57d78 Merge pull request #280 from marcoarnulfo/fix/eq-discrete-redesign
fix(eq): switches and choices redesign the filter on a running EQ (#239)
2026-10-13 04:27:02 -04:00
Brandon Thomas e5787ab7ac Merge pull request #279 from marcoarnulfo/fix/midi-quantize-grid
fix(midi-editor): Quantize snaps to the grid the editor shows (#226)
2026-10-13 04:26:56 -04:00
Brandon Thomas f146bc359f Merge pull request #278 from marcoarnulfo/fix/pencil-plugin-range
fix(edit-window): Pencil clicks on plugin lanes use the plugin's range (#224)
2026-10-13 04:26:53 -04:00
Brandon Thomas 12224df8f5 Merge pull request #277 from marcoarnulfo/fix/hide-floating-automation
fix(window): Hide All Floating Windows also hides Automation (#253)
2026-10-13 04:26:49 -04:00
Brandon Thomas baf102e13f Merge pull request #276 from marcoarnulfo/fix/filmstrip-clip-start
fix(video-universe): filmstrip thumbnails use the clip's own movie time (#225)
2026-10-13 04:26:45 -04:00
Brandon Thomas bd7e8f271f Merge pull request #275 from marcoarnulfo/fix/video-selection-midi-target
fix(midi-editor): a selected video clip is not a MIDI target (#258)
2026-10-13 04:26:42 -04:00
Brandon Thomas b8e206273c Merge pull request #272 from marcoarnulfo/fix/midi-front-keeps-editor
fix(window): keep the MIDI Editor open on Bring to Front (#254)
2026-10-13 04:26:37 -04:00
Brandon Thomas 5c1f9d7dc8 Merge pull request #271 from marcoarnulfo/fix/command-filter-ui-rows
fix(control): apply the engine.commands filter to UI commands (#255)
2026-10-13 04:26:33 -04:00
Brandon Thomas d14edcd51c Merge pull request #270 from marcoarnulfo/fix/track-vca-null
fix(track): vca: null clears a VCA assignment, as documented (#237)
2026-10-13 04:26:28 -04:00
Brandon ThomasandClaude Opus 5.5 ad8beac9b4 Look up the MIDI track offset without allocating on the audio thread
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>
2026-10-13 04:21:20 -04:00
Brandon ThomasandClaude Opus 5.5 165fa98a90 Count tracks, not lanes, in track.clear_trim_automation's result
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 04:21:20 -04:00
Claudio bee999584f Make clip.unloop recognise loops made by clip.loop, even after edits
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.
2026-10-12 23:54:33 -06:00
Marco Arnulfo 40c5e9b42a VST3: hand host-set parameter values to the controller before saving state (fixes #257)
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.
2026-10-13 07:06:22 +02:00
grovebit f188b66209 Leave controller events before a clip's start silent
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.
2026-10-13 02:42:15 +01:00
grovebit eada1c4cfe Fit the section names in narrow Mix strips
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.
2026-10-13 01:36:13 +01:00
grovebit c82b3887f0 Draw faders and meters more like Pro Tools
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.
2026-10-13 01:33:28 +01:00
grovebit 7be845abd1 Transient detection: find quiet and legato onsets, place them on the attack
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.
2026-10-13 00:46:34 +01:00
grovebit b9e4873ffa Tab to Transient: every selected track, Alt+Tab back, Shift+Tab extends
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.
2026-10-12 23:51:18 +01:00
grovebit 8f8884ec05 Refuse to replace the session while bouncing
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.
2026-10-12 23:47:24 +01:00
grovebit d19d44cd1b Cap bogus plugin latency so offline renders can't stall
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.
2026-10-12 23:47:24 +01:00
grovebit 52e19a871a Keep the app responsive while bouncing
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.
2026-10-12 23:47:00 +01:00
grovebit 5755795663 Stream bounces to disk instead of rendering them in memory
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.
2026-10-12 23:46:13 +01:00
grovebit 7f9b629964 Draw each take in the Edit window while it records
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).
2026-10-12 23:43:58 +01:00
grovebit 1cd0233f49 Open a MIDI clip in the MIDI Editor by double-clicking it with the Grabber
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.
2026-10-12 23:42:58 +01:00
grovebit e44c49cada Record NaN and infinite input samples as silence
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.
2026-10-12 23:33:48 +01:00
grovebit 30122bcea1 Recover the takes of a recording a crash interrupted
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.
2026-10-12 23:33:48 +01:00
grovebit 62cb92c4df Never replace or quit a session while it records
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.
2026-10-12 23:33:48 +01:00
grovebit 0b9ead430f Stop the transport when the audio output device goes away
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.
2026-10-12 23:33:48 +01:00
grovebit ddb9c9543d Stop recording and keep the take when the input or the disk fails
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.
2026-10-12 23:33:48 +01:00
grovebit 334cf40506 Autosave after every change, on a thread of its own
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.
2026-10-12 23:32:47 +01:00
grovebit 98d543f0d8 Go silent instead of crashing when the audio engine hits a bug
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.
2026-10-12 23:32:47 +01:00
grovebit 5fff2e8c78 Keep working when the interface hits a bug
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.
2026-10-12 23:32:47 +01:00
grovebit b5ea465274 Autosave each session to its own folder and offer unsaved work at launch
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.
2026-10-12 23:32:47 +01:00
woodgrain816andClaude Opus 5.5 1a49fc70fb File › Open Recent: the last 10 sessions, newest first
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>
2026-10-12 17:31:22 -05:00
grovebit 6fd17d7c0e Keep shortcuts working after Tab
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.
2026-10-12 23:16:11 +01:00
Fabio LussiandClaude Opus 5.5 3432b52373 Keep later meter changes on their bars when an earlier meter is edited
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>
2026-10-12 23:52:50 +02:00
Fabio LussiandClaude Opus 5.5 81ec430341 fix(meter): keep K-weighting stable below ~3.4 kHz sample rates
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>
2026-10-12 23:45:03 +02:00
grovebit 1953f2ecc6 Track height: resize live while dragging, Pro Tools modifiers and Ctrl+Up/Down
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.
2026-10-12 22:11:34 +01:00
webdevtodayjason 950f3e7085 Keep clips in order after Trim to Selection 2026-10-12 16:09:01 -05:00
grovebit 6f8f1e7692 Simplify the Edit window's track rows and plugin slots
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.
2026-10-12 22:04:41 +01:00
grovebit 0b078d2f22 Open a send's level only from its fader icon
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.
2026-10-12 22:04:25 +01:00
grovebit d26d6fec77 Lay out Edit window tracks and plugin slots like Pro Tools
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.
2026-10-12 22:03:32 +01:00
grovebit 766dbc4845 Merge send-level into pt-track-headers 2026-10-12 22:01:49 +01:00
grovebit 72288fc77e Make the Shuffle, Slip, Spot and Grid edit modes work like Pro Tools
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.
2026-10-12 21:49:31 +01:00
Sajidur RahmanandClaude Opus 5.5 7a62ab3be0 Add Clip › New MIDI Clip menu entry and command reachability test
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>
2026-10-13 02:44:59 +06:00