Commit Graph
630 Commits
Author SHA1 Message Date
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
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
Limestudio90andClaude Opus 5.5 acaa91008c mix: a newly adopted instance replaces a matching one in its slot
When a third-party plugin was removed and re-added (same id, same channel
count) and both session updates reached the audio thread in one drain, sync
saw an unchanged slot, kept the old instance and retired the new one as an
unclaimed leftover. The host-side editor belonged to the new, retired
instance, so reopening it failed with "the plugin is gone", and the old
instance kept running with no editor.

The host only hands over an instance when it no longer tracks the one in
the slot, so an instance waiting for a slot now always replaces it (inserts
and instruments). Debug logs record instance creation and release on the
UI thread.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:27:12 +02:00
grovebit 5e7309537e Make an automation pass one undo step, its fader ride included
write_automation merged a pass's points under one key, but the fader
driving the pass merged under its own, so each frame of a ride started
two new undo steps: one second of riding made 120, and in two seconds
everything before the pass was gone from the 200-step history. Stop's
last point was a step of its own as well, because every command,
transport.stop included, ended the gesture.

Engine::hold_merge makes every merged gesture take the held key until
it is released. While playing with a track in a write mode,
write_automation holds the pass's key, so the pass and whatever is
dragged during it are one undo step. A command now ends a gesture only
when it changes the document, so Stop no longer splits the pass it
ends.
2026-10-12 18:13:55 +01:00
grovebit dab2bb956c Don't let Strip Silence delete clips past what one render holds
Strip Silence rendered the whole selection at once, and a render stops
at 2^29 samples over all channels: 93 minutes of stereo at 48 kHz, 46
at 96 kHz. Everything past that read as silence, so the clips there
were deleted. A 105-minute selection on a stereo track lost its clip at
100 minutes.

It now renders at most 2^24 samples at a time, only where unmuted audio
clips play, into a streaming version of the silence detector
(`SilenceScan`, which `non_silent_ranges` now runs on). A window takes
at most 64 MB instead of up to 2 GiB, and more than a day of clips is
refused rather than scanned for minutes. `render_clips` now skips clips
outside its range before looking up their clip effects, so a track of
5,000 clips strips as fast as before. Results are unchanged on the demo
sessions and on 10 minutes of tone bursts that cross window edges.
2026-10-12 18:12:00 +01:00
grovebit edf4fc59f6 Right-click a track's name for the track menu, in Edit and Mix
From #186 and #305: right-clicking a track's name did nothing. In the
Edit window the track menu (Rename, Duplicate, Make Inactive, Hide,
Delete, height, colour…) was only on the header's background, and the
name plate drawn over it took the click. The Mix window had no track
menu at all. Pro Tools opens it from the name in both windows.

Both name plates now open the same track menu on a right-click. Clicks,
double-click to rename and dragging to reorder are unchanged.
2026-10-12 18:07:47 +01:00
grovebit 7ea64c1190 Send levels drag down to −∞, reset with Alt-click and don't stick
From #186 ("Adjusting send levels can get stuck"). The send readout in
the Mix window had three problems:

- A drag stopped at −60 dB, so a send couldn't be dragged off, and a
  new send (−∞) jumped to −60 on the first drag. Drags now move along
  the fader's taper, like the level bar under the send name: smoothly
  down to −∞ and back up, at about the old 0.15 dB a point near 0 dB.
- Its tooltip promised "double-click for 0 dB", but the first click
  already turned the readout into a text field, so that never ran.
  Alt-click now resets it to 0 dB, as on the fader. The volume and pan
  readouts reset on Alt-click too (0 dB, centre), since all three share
  the readout, and Alt-click no longer opens their text field.
- The text field stayed open when it lost focus while hidden (switching
  windows with it open, for longer than a frame), and a later Enter
  anywhere committed its stale text. Now it gives the readout back, and
  Enter only commits while the field has focus.
2026-10-12 18:03:36 +01:00
Limestudio90andClaude Opus 5.5 9e84e0e351 Close plugin editors before reopening the audio device
Applying new audio device settings replaced the player while plugin
editor windows were open: the old instances went away with it (possibly
on the audio thread, where windows cannot be closed) and their editor
windows stayed on screen orphaned. Player::close_editors closes them on
the main thread first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 18:57:05 +02:00
grovebit fe3d759b53 Add tracks from the Tracks list and the empty space below the tracks
From #307: adding a track took the Track menu or Shift+Cmd+N
(Shift+Ctrl+N on Windows and Linux). Nothing on screen offered it, not
even in an empty session.

- The TRACKS header in the Tracks list has a + button that opens New
  Tracks. The list is in both the Edit and the Mix window.
- A double-click in the empty space below the last track in the Edit
  window (header column or timeline) opens New Tracks, as in Pro Tools.
  A single click there still clears the selection.
- A double-click right of the last strip in the Mix window does the
  same. With no tracks, that is the whole window.
2026-10-12 17:52:12 +01:00
Limestudio90andClaude Opus 5.5 2e3389178e Mix in a separate window: Edit and Mix on screen at once
Window > Mix in Separate Window (window.mix_separate, kept with the UI
preferences) shows the Mix window in its own native window through an
egui immediate viewport, so Edit and Mix can be seen at once, e.g. one
per monitor. The main window then keeps Edit; Window > Mix and
Cmd/Ctrl+= bring the Mix window to the front; closing it puts Mix back
into the main window. Backends without multiple windows show it as a
floating egui window.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 18:47:18 +02:00
grovebit 5026b0655d Signal Generator fills the selection with a new clip
Fixes #220: Render in the Signal Generator window did nothing on a time
selection over an empty part of a track. AudioSuite only processed the
clips under the selection, so with none it stopped with "select audio
clips first", and that went to the status line only.

- A generator (soundcraft_dsp::is_generator, today only the Signal
  Generator) ignores its input, so it no longer needs clips. With a
  time selection it renders a new clip over exactly the selection on
  each selected audio track, over whatever was there, as Pro Tools'
  Signal Generator does. Before, it replaced whole clips under the
  selection and left the empty parts empty.
- Other tracks in the selection are skipped. With no audio track in
  it, or no time selection and no clips, it reports why.
- Explicit `clips` still process those clips. Effects behave as before.
- The AudioSuite window's hint says what Render will do for a
  generator.
2026-10-12 17:38:17 +01:00
grovebit 3a702d9c6f Refuse renders too long to finish instead of cutting the audio short
One offline render holds MAX_RENDER_SAMPLES samples, about 93 minutes
of stereo at 48 kHz, and the offline time stretch, pitch shift and
resample stop at MAX_OFFLINE_SAMPLES frames. Longer audio was cut short
without a word: Render Clip Gain and Clip Effects Render left the end
of the clip silent, every AudioSuite process shortened the clip, the
track Commit made fell silent at the limit, and Export Clips wrote
shortened files. Above 74 kHz an hour no longer fits, so TCE, Elastic
Properties and Remove Pitch Shift padded the cut end with silence.

Each now checks the length before it renders and refuses with the
limit in seconds, as Consolidate does in #212. AudioSuite counts what
Time Stretch, Pitch Shift and Varispeed make, so a fourfold stretch is
refused at a quarter of the limit. Export Clips checks every clip
before it writes a file.
2026-10-12 17:35:33 +01:00