Commit Graph
838 Commits
Author SHA1 Message Date
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
highperfocused d6ea8cf001 fix(ui-egui): own the test palette lock for every SoundApp lifetime
Parallel test runs could flake: theme::LIGHT_THEME is process-wide, and
the edit_window harness tests ran whole frames without holding
lock_test_palette(), so a theme test could flip the palette between a
harness test painting rows and looking them up by colour (and a harness
frame could reset it to dark mid-theme-test).

Make the test palette lock re-entrant per thread and take it inside
SoundApp::new (test builds only), so every test that builds an app owns
the palette for the app's whole lifetime. Adds a regression test that
the lock is re-entrant on the owning thread and excludes others.

Fixes #323
2026-10-12 22:43:33 +02:00
webdevtodayjason c39773b62d Keep automation thinning within its tolerance 2026-10-12 15:32:48 -05:00
webdevtodayjason 34d8488284 Keep the clip gain line where a clip is trimmed or separated 2026-10-12 15:32:40 -05:00
webdevtodayjason 8716dc9025 Add Jason Brashear to the contributor names 2026-10-12 15:32:40 -05:00
grovebit a47ce1cee5 Waveforms like Pro Tools': exact per-pixel peaks and working view options
Waveforms looked blocky and jagged next to Pro Tools': each column was a
point wide (two pixels on Retina screens), and the overview only knew
32-sample blocks, so zooming in showed stair steps and, at the sample
level, wide flat bars instead of the signal. Several View > Waveforms
options did nothing or drew something else.

- Peaks keeps one tree of blocks (min, max and sum of squares). Its
  queries read the partial blocks at a range's edges from the samples,
  so every answer is exact and still O(log n). `Peaks::spans` gives each
  column the (min, max) of the line through the samples over its span;
  neighbouring spans share their boundary, so the waveform stays
  connected at any zoom.
- The Edit window draws a column per physical pixel, each over a
  1-point window anchored to absolute sample positions (no shimmer while
  scrolling), with anti-aliased ends, in one triangle strip where runs
  of equal columns such as silence share vertices. At the sample level
  (a sample per pixel or less) the waveform is a 1-point line through
  the samples, without Rectified, Power or Outlines, as in Pro Tools.
- Outlines draws an edge around a lighter fill, as Pro Tools does; it
  used to replace the fill with a line of dots along the tops.
- Power shows the RMS, which is how Pro Tools calculates it
  (`Peaks::rms_spans`). Ours squashed the peak heights with a square
  root, which drew loud material bigger than Peak view. Power draws
  +/-RMS around the centre, or 0..RMS above the base line when
  Rectified.
- Dark theme clips have a dark body tinted with the clip colour and a
  light waveform in that colour, as in Pro Tools, instead of black
  waveforms on a mid-tone body; a selected clip inverts to a bright body
  with a dark waveform, as before. MIDI notes on unselected clips take
  the waveform colour, with velocity as opacity. The Light theme is
  unchanged.
- Blocks view draws clips as empty blocks with their names, and in
  Playlists view the alternate playlists' lanes draw their clips like
  the main lane, waveforms included, instead of empty boxes.
- Overlapped Crossfades draws the outgoing clip's waveform, translucent,
  over the crossfade, so both clips' audio shows inside it. The outgoing
  clip is found by binary search in the clip's own playlist. A crossfade
  also shows both fade curves: the outgoing clip's fade-out used to be
  hidden under the incoming clip.

Tests: the peaks unit tests check every range, span and RMS against the
samples; tests/waveform.rs checks the sample-level line, a one-sample
spike zoomed out, Blocks and playlist lanes, and Overlapped Crossfades.
2026-10-12 21:18:25 +01:00
grovebit 921a36aa45 Clicking a send's level opens its Send window with a fader
In Pro Tools, clicking a send opens its Send window: the send's fader,
pan, Mute and Pre/Post. SoundCraft turned the send's level readout into
a text field instead, so there was no fader to grab.

- A click on a send's level in the Mix window opens the Send window:
  the destination, Pre and Mute buttons, a pan knob, the fader (Alt-
  click for 0 dB, Cmd/Ctrl for fine moves, down to −∞) and the level,
  which takes a typed value. It stays open while you play.
- One Send window at a time, as with Pro Tools' Target lit: clicking
  another send's level moves the window to that send, and clicking the
  same send again closes it. The send shows an outline while its window
  is open. Removing the send or its track closes the window.
- Escape closes it too, unless the Escape is for something on top: an
  open menu, a level being typed, a dialog or the search. egui closes
  menus and drops text focus on Escape before the window is drawn, so
  that is read from the previous frame.
- Dragging the level and Alt-click on it still work in the strip, and
  right-click still opens the options menu.
- The send's level and the window's fader have accessibility labels
  ("Send A level", "Send level").
2026-10-12 21:17:36 +01:00
grovebit 3617011342 Pro Tools keys for nudging, Grid and Nudge values, playlists and windows
Commands that already worked but had no key get the ones Pro Tools uses,
on macOS, Windows and Linux alike (Alt is Option on macOS; Cmd is Ctrl
elsewhere):

- Shift+= and Shift+- step the Grid value to the next larger or smaller
  one, and Alt+Shift+= and Alt+Shift+- the Nudge value, as the Pro Tools
  Reference Guide gives them. `view.grid` and `view.nudge` take `step`.
  `GridValue::stepped` walks the note values, keeping a dot or triplet, and
  steps seconds, frames and samples by tens. macOS sends Alt+Shift+= as =
  (the key under ±), so that arm takes = as well as +. Both commands list
  the keys as descriptive shortcuts, like Nudge's Plus/Minus, so they show
  wherever commands list their shortcuts; the strings don't parse, so the
  registry never dispatches them and `shortcuts::fixed` handles the keys.
- Commands Focus: , and . nudge back and forward, M and / nudge by the next
  larger Nudge value (`edit.nudge` takes `next`), 0 copies the Timeline
  selection to the Edit selection and O the other way, [ and ] play the
  Edit and Timeline selections, and Shift+Z redoes. Shift+P and Shift+;
  add the track above or below the selection, keeping its range (P and ;
  already move it). They run Extend Edit Up/Down, which now skip tracks
  the Edit window doesn't show (hidden, or in a closed folder); before,
  they could add an invisible track. Shift+; can arrive as :, so both are
  matched.
- Playlists: Alt+Shift+Up and Alt+Shift+Down copy the selection to the
  target playlist and to a new one, Alt+Shift+T and Alt+Shift+N move it
  there, Shift+Right shows the target playlist and Cmd+Shift+Right
  designates the shown one. The MIDI editor's piano roll moved notes on any
  arrow key, whatever the modifiers; it now leaves Alt, Cmd and Ctrl arrows,
  and Shift+Left/Right, to these shortcuts.
- F12 records, as in Pro Tools. Cmd+Space is Spotlight on macOS.
- Ctrl+Tab goes back to the previous clip boundary off macOS, where Windows,
  GNOME and KDE take Alt+Tab; Pro Tools for Windows uses Ctrl+Tab too.
  Cmd+Alt+Tab now toggles Tab to Transient: the Tab arm took it as Alt+Tab
  and tabbed back instead.
- Windows: Alt+7 Universe, Alt+6 Clip Effects, Alt+= MIDI Event List,
  Shift+K MIDI Keyboard, Cmd+Alt+J Window Configuration List, Alt+' Task
  Manager, Alt+I Workspace, Alt+J Workspaces to Front.

Tests drive the keys through `shortcuts::handle` for each OS
(tests/pro_tools_keys.rs), plus the piano roll with modifier arrows held.
2026-10-12 21:17:26 +01:00
grovebit bebca3aa85 Draw grid lines over clips in the Edit window
The Edit window painted the bar and grid lines on the empty lane and then
the opaque clip bodies on top, so the grid vanished wherever an audio or
MIDI clip sat. Pro Tools draws grid lines over the clips.

- While grid lines are shown, the lines are drawn after the clips (audio,
  MIDI, video and their overlays) and before automation and the selection.
- The lines are a translucent white (Dark) or black (Light), worked out
  from the palette's line and lane colours, so a line on an empty lane looks
  as before and waveforms and notes stay readable under it. Bar lines now
  also stand out where a grid line lands on them; the opaque grid line used
  to cover them.
- With grid lines hidden, the bar lines stay behind the clips as before.

Test: tests/grid_lines.rs renders the demo Edit window in both themes and
checks that the lines over the Kick clip are painted after it only while
grid lines are shown, and that each one looks like the theme's line colour
on an empty lane.
2026-10-12 20:43:09 +01:00
highperfocused c8021d4c90 fix(mix): mix.insert_params rejects unknown parameter names
Unknown keys in `params` were silently dropped, so a typo like `gain_db`
reported `ok: true, set: 0` — indistinguishable from a successful no-op.
The command now fails and lists the unrecognized keys (and non-numeric
values), matching how `mix.insert` already validates its params. Preset
recall in the plugin menu drops stale keys before calling the command.

Fixes #266
2026-10-12 21:41:43 +02:00
grovebit c9b694907f Mark the Tracks panel's edge with a grey scroll bar
The Tracks and Groups lists met the Edit and Mix windows at a faint
one-point line. A 10-point grey gutter with dark edges now runs down
the panel's right side and carries each list's scroll thumb when the
list is longer than its space: drag it, or click above or below it to
page. egui's own scroll bars are off there.
2026-10-12 20:27:53 +01:00
grovebit 312315acb9 Type to search as soon as a plugin or bus menu opens
Finding a plugin for an insert took three submenus (format, vendor,
name). The send and I/O bus menus had a search field, but it had to be
clicked before typing, and it kept the last search.

- The send and I/O bus menus take typing as soon as they open, as Pro
  Tools' path selectors do. The search starts empty each time; Up/Down
  move through the matches, Enter picks one, Escape closes the menu.
- Insert and instrument menus start with the same search field. Typing
  lists the plugins whose name or vendor matches: built-in ones first,
  then Audio Units and whichever of CLAP and VST3 have been scanned,
  with a Scan button for each of the others. Typing never starts a scan
  (`cached()` in the CLAP and VST3 hosts returns the last scan without
  scanning); opening a format's submenu still scans it.
- A click inside the insert and instrument menus no longer closes them,
  so the field can be clicked; picking still does, and errors from
  Bypass and "no insert" now reach the status line. The send bus menu
  is no longer 600 points wide.
- Insert, send, instrument and I/O slots in the Mix window get
  accessibility labels, which the tests use.
2026-10-12 20:21:55 +01:00
reoniazx 5ec8c21b19 Preview waveform amplitudes during fade drags 2026-10-13 02:48:00 +08:00
reoniazx 79e30c39d8 Smooth fade curves and retain draggable midpoint controls 2026-10-13 02:48:00 +08:00
reoniazx 4ac47b3a7f Add draggable midpoint curvature for clip fades 2026-10-13 02:48:00 +08:00
reoniazx d5e6fa32ef Enable dragging existing fades from their curve endpoints 2026-10-13 02:48:00 +08:00
reoniazx db23f873fd Match fade drag previews to the committed curve 2026-10-13 02:47:59 +08:00
reoniazx d9922c47a6 Fix clip fade drag bounds and targeted fade creation 2026-10-13 02:47:59 +08:00
grovebit 57682b03ef Merge silver-light into pt-track-headers 2026-10-12 19:24:54 +01:00
grovebit 3189a1a512 Tint each Edit window track with its colour
As in Pro Tools, a track's colour now runs through its whole row
instead of only the strip at its left edge: the header (and any
header columns) takes a muted shade of it and the lane a faint one,
so clips in the full colour still stand out.

- Window > Color Palette has a "Track tint in the Edit window" slider;
  at zero tracks look as before. The setting is saved with the UI
  prefs.
- A selected track's header gets an accent outline, since its blue
  background would otherwise be lost under the tint.
2026-10-12 19:23:53 +01:00
grovebit 4c38ef59c1 Silver Light theme
The Light palette was near-white with a blue tint. Pro Tools' lighter
look is silver, so panels, strips, lanes, rulers and keys move to
neutral greys between 184 and 236, and meters sit in dark wells as they
do in Pro Tools. Borders and text are darkened to keep their contrast,
borders a little more so that tracks, Mix sections and controls don't
run into each other. Dark is unchanged.

Part of #138.
2026-10-12 19:22:44 +01:00
grovebit 6e361262e1 Hardware-style knobs, faders, keys and meters
The shared controls were flat rectangles and discs. They now follow Pro
Tools' shapes and colours, drawn in code with vertex-coloured meshes (no
assets), and keep a modern, soft shading:

- Faders: a silver cap, taller than wide, with a dark centre line and
  faint grip ridges, in a slot that leaves room for the cap at both
  ends.
- Pan knobs: a softly shaded disc with a light rim and a white pointer
  beside the green value arc.
- Keys (S/M/I, record arm, toolbar and transport buttons, edit modes,
  Grid, Count Off, selector boxes, filled insert, send and instrument
  slots): lit from above and sunk while pressed. Record arm shows an
  unlit salmon light when off. The edit-mode keys take their width from
  the widest label, so SHUFFLE and REL GRID fit.
- Meters: LED segments with thin gaps, the unlit ones faintly visible.
- Mix sections and the pan and volume readouts sit in outlined wells.
  Strips get a soft gradient and a lit left edge, track headers a lit
  top edge, and a line separates the Edit window's headers from the
  lanes.
- Filled insert slots are grey, as in Pro Tools. They no longer
  brighten on hover: on the grey, that dropped their label below 4.5:1
  in Dark. mix_instrument_contrast now finds the key's face and rim,
  and checks that hovering keeps the face.

Part of #138.
2026-10-12 19:20:02 +01:00
grovebit 2e5fc17f69 Merge edit-plugin-columns into controls-look 2026-10-12 19:13:27 +01:00
grovebit e53b772aea Open, add and change plugins from the Edit window
Pro Tools lets you work with a track's plugins from the Edit window.
Here its Inserts and Sends columns (View > Edit Window Views) only
printed the names.

- The Inserts A-E/F-J and Sends A-E/F-J columns now use the Mix
  window's slots, so they work as they do there: click a plugin to open
  its window, click an empty slot to insert one, right-click to bypass,
  remove or replace it, and route or set the level of a send.
- A new Instruments column shows an instrument track's instrument.
- The columns run in Pro Tools' order: Instruments, Inserts, Sends, I/O,
  Comments. Each gets a title, boxed in its own row under the rulers,
  and the ruler names now end at the track headers.
- A Views button at the top left of the rulers shows and hides the
  columns, like Pro Tools' Edit Window View selector. Its menu stays
  open until a click elsewhere, so several columns take one trip.
- In both windows, insert slots show a power light, green while the
  plugin runs and dark while it's bypassed, with the name left-aligned
  and dimmed when bypassed. A plugin whose code is missing shows its id
  instead of looking like an empty slot.
- Cmd+click (Ctrl+click on Windows and Linux) bypasses an insert, as in
  Pro Tools.
- Each column draws in a Ui of its own, so its widget ids can't clash
  with the track header's.
2026-10-12 19:11:33 +01:00
grovebit 66caedd923 Line up stereo Mix strips with mono ones
Stereo strips draw two smaller pan knobs (32pt or less instead of 34pt),
and the rows under them were placed from the knob size. So the pan
readouts, the I/record and S/M keys and everything below them sat 2pt
higher than on mono strips. The rows now use the mono positions on every
strip.
2026-10-12 19:06:19 +01:00
grovebit 6d03eab566 Make the fader scale readable in the Light theme
The fader's dB numbers and ticks were painted in fixed greys (150 and
110), which are faint on the Light theme's strips: the numbers had a
2.5:1 contrast. They now use the theme's text_dim, at 70% for the ticks.
Light reads at about 5:1 and Dark looks the same as before.

Each tick now starts 2pt after its number instead of running through the
digits, and is left out when the strip is too narrow for it.
2026-10-12 19:06:13 +01:00
grovebit 6b3c73b7ff Decode each audio file once when a session names it many times
Opening a session decoded every source entry on its own, so a 400 KB
session listing one 11 MB WAV 2,000 times took 16 s and 22 GB, and
20,000 entries never finished. Import Session Data and clip group files
did the same.

All three now decode a file the first time it is named, by its
canonical path, and share the decoded audio with later sources that name
it; the pool's audio is never changed in place, so sharing it is safe.
Saving keeps sources that play one file on that file, and an autosave or
a copy writes shared audio once instead of once per source. That session
now opens in 0.3 s at 110 MB, and a normal session bounces to the same
bytes as before.
2026-10-12 18:40:12 +01:00
grovebit 328760fd70 Keep positions from files within the range commands use
Commands clamp every position they take to ±2^40 samples so the
arithmetic after them can't overflow, but session and clip group files
skipped that. A clip, MIDI note, marker, automation point or selection
near i64::MAX made Duplicate, Insert Silence, Insert Time, Heal, Trim to
File Boundaries, Quantize to Grid, Separate on Grid, Thin Automation,
Copy, Move Selection, Extract Chords and Track Commit overflow: a panic
in debug builds, wrapped positions in release.

MAX_POSITION moves to soundcraft-time so the model can use it. Loading
a session clamps clip starts, lengths, offsets, sync points, gain
envelopes and MIDI notes, automation points, markers, key changes, the
selection, playhead, pre/post-roll, scroll and timecode start. A clip's
repairs are now Clip::sanitize, which clip group import uses too, and
Session::sanitize visits each collection once. The Edit window adds a
clip's offset to its file's time stamp without overflowing.
2026-10-12 18:40:12 +01:00
grovebit fe1d7e4c58 Refuse MIDI files that run longer than a day
A MIDI file of 317 bytes, with one note whose end lies 41 maximal
deltas after its start, imported as an 8.5-year clip. Everything that
then rendered or walked that clip (Track Bounce, Commit, Separate at
Transients, Extract Chords) worked for seconds to forever and used up
to 3 GB.

Importing now refuses a MIDI track that ends more than 24 hours after
the import point; its end is computed with saturation, so a huge one
can't wrap around and slip past the check. A file with more than 10,000
meter changes is refused too: placing each one walks those before it,
so 200,000 of them (1.8 MB) took 57 s and fit easily within a day.
2026-10-12 18:40:12 +01:00
grovebit 1b8baa10af Keep Extract Chords to a bounded walk over the selection
Extract Chords walked the selection bar by bar, looking at every note
of the selected tracks in each bar, until it had 2000 chords or reached
the end. After Select All over a session file with a clip at the far
end of the timeline (2^50 samples) it ran for ever over empty bars, and
a 317-byte MIDI file with one note held for 8.5 years kept it busy for
millions of bars that never form a chord.

It now takes the notes in time order, keeps only those sounding in the
bar it looks at, jumps over bars where nothing sounds, and stops after
100,000 bars that hold notes (27 hours at 120 BPM) or 10 million
sounding notes. The chords it finds are the same.
2026-10-12 18:40:12 +01:00
grovebit 7ca978fdb9 Cut names from files to a length that can be drawn
A MIDI file whose track name was a million characters long imported
fine, and the next frame crashed the app: drawing that name needed a
320 MB vertex buffer, past wgpu's 256 MB limit, and the panic comes
from painting, outside anything that catches it. Saved, the session
crashed again on every open. A 4 MB status line in ui.json did the
same at every launch.

Names read from files are now cut to 256 characters and comments to
4096: everything Session::sanitize repairs on load (session, track,
playlist, clip, marker, bus, output, group, source, video and key
names), MIDI track names on import and clip names from clip group
files. The status bar draws at most 1024 characters.
2026-10-12 18:40:12 +01:00
grovebit a8a36540d6 Name thousands of same-named tracks and files in linear time
Finding a free name tried "Name 1", "Name 2" and so on, rescanning
every track (or every source path) for each try, so each new name cost
time proportional to the square of the session and a bulk import the
cube. Importing a 119 KB MIDI file with 4,095 tracks all named "Piano"
took 28 seconds; a clip group file with 8,000 empty lanes or 8,000
audio files of one name ran for over a minute, and 100,000 lanes never
finished. Import Session Data from a session with 2,000 same-named
tracks took 7 seconds.

unique_track_name and the new audio file name now collect the numbers
already in use once and take the first free one, which gives the same
names. A clip group file also counts each empty lane and each audio
file against its limit of 10,000. The same imports now take 1 second
(MIDI), 2 seconds (8,000 lanes) and a third of a second (Import Session
Data); 100,000 lanes are refused.
2026-10-12 18:40:12 +01:00
grovebit d81d5756d4 Check an import's size at the session's sample rate
An 80 KB WAV with a sample rate of 1 Hz froze the app on import: the
size limit was checked before conversion to the session rate, which
then made it 48,000 times longer, past 2 GiB of memory and more than a
minute of work (4.5 GiB and 75 s in an earlier measurement). Opening a
session, Import Session Data, the sampler and clip group files
converted without any limit.

io::at_rate checks the size the audio will have at the session's rate
before converting it. Imports keep their limit (2^30 samples, about
4 GiB) and their message; the sampler keeps its own limit; a session's
source that is too large is reported like other unreadable media.
2026-10-12 18:40:12 +01:00
grovebit 7f4ab5c76b Refuse devices and pipes where a file is expected
A session whose audio file path pointed at /dev/zero filled 18 GB of
memory while opening, and one pointing at a named pipe waited forever.
The same went for a path given to the import commands, the sampler,
Import Session Data, clip group and track preset files, and the session
file itself, all of which read whatever the path named to its end.

Those reads now go through io::read_file / read_text, which refuse
anything but a regular file. A session's missing audio is reported as
missing, as before; an import fails with "not a regular file".
2026-10-12 18:40:12 +01:00
grovebit 1617733a98 Repair a session's sample rate and tempo map when it loads
A session file with a sample rate of 0 or a tempo of 4e9 BPM opened
fine, but Strip Silence on the first and Extract Chords on the second
never finished (one also climbed past 1 GiB). Session::sanitize, which
repairs everything else read from a file, skipped both fields.

On load, a sample rate outside what sessions accept becomes 48 kHz, and
the tempo map keeps only the tempos and meters that set_tempo and
set_meter accept, in tick order, with one of each at tick 0.
2026-10-12 18:40:12 +01:00
grovebit de5f2e0fb3 Check INFO entry sizes in WAV files that symphonia reads
WAV files that aren't plain PCM (A-law, µ-law, ADPCM) go to symphonia,
which reads every entry of an INFO list into a buffer of its declared
size, bounded only by the list's and file's declared lengths. A file
claiming a 4 GB RIFF and a 2 GB title asked for 2 GB before failing.
macOS gives that lazily; a 32-bit build or a system that can't commit
it aborts.

Before symphonia reads such a WAV, its INFO entries are checked against
the bytes the file holds, and one that claims more is refused as
malformed. Chunks are walked where symphonia reads them, not where their
lengths say: A-law, µ-law and ADPCM `fmt` chunks are read by their own
extra size, extensible ones by 40 bytes, and a list is padded twice.
2026-10-12 18:39:48 +01:00
grovebit 777c08ef1a Skip ID3 tags instead of having symphonia read them
An MP3 whose ID3v2 tag had a frame claiming 0x7FFFFFFF bytes made
symphonia's probe allocate 2 GB to read that frame (4 GB at
0xFFFFFFFF). It reads every tag it finds ahead of the audio, though
SoundCraft never uses the result. macOS gives such memory lazily; a
32-bit build or a system that can't commit it aborts.

The probe loop now skips an ID3v2 tag by the size in its header and
carries on looking for the audio, so a tag can't cost more than the
bytes it occupies, and a damaged tag no longer stops an MP3 from
opening.
2026-10-12 18:39:48 +01:00
grovebit a4213cf90f Check FLAC picture and comment lengths before symphonia reads them
symphonia reads a FLAC file's picture and Vorbis comment blocks field
by field, allocating each field's declared length before it finds out
the block is shorter: a picture claiming 0xFFFFFFFF bytes of image
data asked for 4 GB. macOS gives that lazily, but a 32-bit build or a
system without that much memory to commit aborts.

When the probe finds a FLAC stream, the lengths of its picture fields
and comments are now checked against their metadata block first, and a
file whose fields don't fit is refused as malformed.
2026-10-12 18:39:48 +01:00
grovebit d64d5f491e Refuse files whose packets claim millions of frames
A 59 KB ALAC file in an M4A made the app use 25 GB of memory before it
gave up: symphonia's ALAC decoder sizes its buffers from the frame
length in the file's codec settings, and nothing bounds it. The PCM and
ADPCM decoders likewise allocate the frames per packet the container
declares, which an MP4 version-2 LPCM entry takes from the file.

Before a decoder is made, the frames a packet may hold, and an ALAC
frame length, must be at most 2^20 (about 22 seconds at 48 kHz;
Apple's ALAC encoder uses 4096), or the file is refused as malformed.
2026-10-12 18:39:48 +01:00
grovebit 37f240ed54 Don't crash on MP4 files that claim more data than they hold
An M4A whose handler atom claimed a 64-bit size of 2^60 bytes ended the
process on import: symphonia reads `hdlr` and the `data`, `name` and
`mean` atoms of iTunes metadata into buffers of their declared size, and
an allocation that fails aborts. Its sample tables reserve room for
whatever entry count they claim (up to 68 GB from a 19 KB file), and
every sample is read into a buffer of the size `stsz` gives it. macOS
hands out most of that lazily; on Linux, Windows and the 32-bit Windows
build such a request fails and aborts.

When the probe finds an MP4 file, the atoms symphonia descends into are
now walked first, the way symphonia reads them: a header that starts
inside its container is read whole, and the walk ends where symphonia
stops, after an atom that runs past its container. A file is refused as
malformed when one of those atoms, a sample table or a sample claims
more bytes than the file holds. Atoms symphonia skips, and an `mdat` cut
short, are left alone, so truncated recordings still read.
2026-10-12 18:39:48 +01:00
grovebit c40078f32a Don't crash on CAF files that claim more data than they hold
Importing a 72-byte CAF file ended the process. symphonia allocates
whatever a CAF magic cookie, packet table or packet claims before it
reads it, and when that allocation fails Rust aborts, which no
catch_unwind can stop. A cookie claiming 54 PB, a table of 2^50
packets and a single packet of 2^50 bytes each did it, on import and
in `soundcraft-cli info`.

symph::open now runs symphonia's probe loop itself, and when the
container it finds is a CAF stream, it first walks the chunks the way
symphonia reads them. A file whose cookie, packet table or packets
claim more bytes than it holds is refused as malformed. So is a packet
table symphonia would refuse only after reserving 32 bytes per packet:
one that runs past the file, or one listing packets of a constant bit
rate stream. The probe's hint was never read by symphonia 0.5, so it is
no longer passed.
2026-10-12 18:39:48 +01:00
grovebit 0a5fc6ccba Say at the Record button why Record did nothing
Fixes #316: in a new session, clicking Record did nothing visible. With
no record-enabled track there is nothing to record, and the reason went
only to the end of the dim status line at the bottom of the Edit
window. In a new session it also pointed at "the red button in its
header", which no track existed to show.

- Record with nothing to record on says what to do: add a track with
  Track › New… when the session has none, else click R on the track
  to record.
- The message shows for 4 seconds below the toolbar's Record button
  (under the whole transport group, so it covers none of its buttons)
  and under the Transport window's Record button, as well as in the
  status line. It goes early once a track is record-enabled.
- Nothing is armed or created on the user's behalf, and the transport
  stays stopped.
2026-10-12 18:33:03 +01:00