The piano roll's rect stayed recorded after its tab went behind another
one, so with the pointer over the panel now in front, Left and Right
still went to the hidden piano roll and the Edit window did not scroll.
The rect is now recorded only by a frame that draws the MIDI panel.
With a selection in the Edit window, plain Left scrolls the view so the
selection's start is in the middle of the timeline, and Right its end.
Without a selection the keys do nothing, as before. With the pointer over
the MIDI editor the arrows keep moving notes there.
Strip Silence always opened at -48 dB / 50 / 5 / 20 ms. Its four values are
now kept for the session next to the strip names and written back each frame
the window is shown, so Escape, Cancel, Strip, Extract, Separate and the
Rename round trip all keep them.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A session was parsed into a serde_json Value and then converted, which takes
many times the file's size in memory, and nothing limited how big a session
file could be.
Session::from_json now reads only format_version first (every other field is
skipped) and then parses the text straight into the session. A missing
format_version and a newer version give the errors they gave before. Other
type errors now end with the place they were found ("at line L column C").
A field of the document given twice in one object, which used to load with
the last value winning, is now refused with "duplicate field `key`" (a
twice-given format_version too, not "missing format_version"): keeping the
old leniency would need the tree this change removes, and no writer repeats
a field. Keys of maps (plugin params, edit values and the like) and unknown
keys still keep the last value given, as before.
A track's height was read through an untagged enum, which buffers the whole
value before it looks at it; a huge array there took about 27 times its size.
It now has its own reader that takes exactly the forms that loaded before
("Micro" to "Extreme", also as {"Micro": null}, {"Custom": n}, {"Fit": n},
and the old bare "Fit" or {"Fit": null}) and refuses anything else at its
first token.
Opening a session (a .scraft file, the browser's bytes, the session in a
bundle) and Import Session Data refuse one over 256 MiB by its size, before
reading it. Save, Save Copy In, Save As Template, autosave and the browser's
bundle refuse to write a session over that size, so SoundCraft never saves
one it then won't open. Save and autosave check before they write any audio,
so a refused save leaves nothing behind and a refused autosave keeps the last
one and its audio copies as they were; the refusal is reported like any
other autosave failure. Save checks against the longest name each new audio
file can get, so it may refuse a session within a few bytes per new file of
the limit. Crash recovery reads only the name of a leftover autosave, and
none from one over the cap. Audio files are read without that cap, as
before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On Windows and Linux on_main_thread() always said yes, so any thread could
open, idle or close a CLAP editor, and an instance dropped on another thread
destroyed its editor there. CLAP allows clap.gui calls on the main thread
only.
It now uses the rule the CLAP host's Windows window code and the VST3 host's
module loading already use: the thread Rust names "main", where the app runs
its UI. The editor tests check that another thread, named or not, is refused
on every platform, and that a plugin dropped on another thread with its
editor open warns and leaves the editor instead of destroying it there (the
test plugin now allows that, and only that, at destroy).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(ui): About credits tables fit without side-scrolling and their columns resize
The Contributors table and the Models table were egui Grids inside a
two-way scroll area: they opened wider than the dialog, so they scrolled
sideways, and Company and Model sat almost touching. Both are now
egui_extras tables. Each column opens at the width of its header and widest
cell, the spare width goes to the text columns (Name; Company and Model),
and every column edge can be dragged. Widening a column grows the table
like a spreadsheet, and only then does it scroll sideways; the header stays
put while the rows scroll. Counts and dates are right-aligned and every
cell is vertically centred.
The sorted column stays highlighted and its direction shows on the
toolbar's sort-order button, so headers no longer reserve room for an
arrow. The opening widths come from fit_widths, a pure function with unit
tests that a fitted table fills its room exactly and that only the text
columns give way when room is short.
The About dialog opens 820 points wide, so every column fits at its natural width.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Let every credits column be dragged, and refit changed tables
The last column was egui's remainder column, which has no drag handle, so
it could only be resized from the edge before it. It is now an ordinary
column with its own handle.
A table keeps its column widths, so switching to a longer name left the
Name column at its old width and clipped the text. The table now goes
back to the opening fit whenever that fit changes: a longer widest cell
or a resized window. Dragged widths are dropped at that point.
fit_widths could overflow the room when one column hit the minimum,
because the other columns did not take up its share. Columns now give
way in proportion to their width above the minimum.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Refit credits tables on what they measure, not the fitted widths
Comparing the fitted widths missed the case it was meant for. Name is the
only growing column, so a longer name takes its extra width from the
spare room and the fit comes out the same: a dragged Name width stayed,
clipping the longer name.
It could also drop dragged widths by accident. The fit comes from the
available width inside the scroll area, which scrolling can nudge, so
scrolling a widened table could reset it.
The table now compares the natural widths and the clip rect's width,
which scrolling does not move.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Converting clip gain to volume (and baking volume into clip gain)
evaluated the clip's gain envelope by walking it from the start for
every breakpoint being converted, so a large envelope in an opened or
imported session cost envelope points squared and blocked the engine.
Build a running-maximum index over the envelope once per clip and
binary-search it. It returns exactly what the walk returned, including
for envelopes stored out of order.
* Tempo map: index tempo segments so tick/sample conversions are O(log n)
Every tick<->sample conversion walked all earlier tempo changes, so code
that converts many positions (MIDI import/export, the tempo ruler, score
layout, MIDI scheduling and controller chasing on the audio thread)
cost positions x tempo changes. A session or MIDI file with many tempo
changes and many notes or tracks could stall the engine or the audio
callback.
TempoMap now keeps the elapsed time at each tempo change (rebuilt on
every edit and on deserialize, never serialized) and binary-searches
the segment. MIDI scheduling also skips notes whose on and off both
fall outside the block before converting them.
* Tempo map: index a file's ticks with saturating subtraction
Bundle entries are stored, so the files read out of one can only
outgrow the bundle when several entries point at the same bytes. A
small bundle naming one payload thousands of times was copied out once
per name, up to the 1 GiB total limit, in the browser. Check the
archive for overlapping entry data before reading any entry.
When the aux/folder routing has a cycle, processing_order sorted the
auxes by a bounded feeder depth recomputed inside the sort key: each
evaluation re-searched every aux's output and sends against a growing
list of inputs, for up to eight hops, on every comparison. A session
with many auxes sharing buses made that cubic during audio sync.
Compute the same capped depth for every track at once: per hop, collect
the buses written by auxes reached so far, then mark the tracks reading
them. The result matches the old search (tested against it on a mixed
graph with cycles) and the sort uses it by lookup.
apply_params searched all of the track's automation lanes for every
stored insert parameter on every block, so a session with many
parameters and many lanes multiplied the two on the audio thread.
Collect the slot's lanes once per block, order them by parameter name
(keeping the first lane per name, the one the old search found) and
walk them alongside the sorted parameter map. The lane list reuses a
scratch buffer kept on the plugin slot.
libloading 0.9 prints only "dlopen failed" ("dlsym failed", "LoadLibraryExW
failed") and keeps the dlerror text or the Windows error as the error's source.
The CLAP and VST3 loaders formatted the error alone, so a plugin built for a
newer glibc was reported as "dlopen failed" with no reason. Append the source.
* MIDI: index controller dedup by channel and kind
Remove Duplicate Notes rescanned every controller already kept at the
same tick for each incoming one. A session whose sequence holds one tick
with many distinct channel/controller pairs made that quadratic and
blocked editing. Track where each channel+kind sits at the current tick
in a map instead; the last event per key still wins.
* MIDI: tag dedup index entries with their tick instead of clearing per tick
Clearing the map on every tick change cost O(capacity) per tick after a
dense tick. Store the tick with each entry and treat entries from an
earlier tick as absent. Add an oracle test comparing against the
previous per-tick rescan on mixed-tick input.
Its help said `<plugin id>`, but it only runs built-in plugins, and a
hosted id (clap:, vst3:, au:) failed as "unknown process". The help now
says built-in plugin id, and a hosted id gets an error saying AudioSuite
runs built-in plugins only.
Fixes#1301.
Bounces passed no bwf to the encoder, so mix and stem WAVs had no bext
chunk and post tools placed them at zero. Bounce::new now stamps them
with TimeReference = the session's timecode start plus the bounce start,
in samples since midnight, as other DAWs do. A caller's own bwf wins;
AIFF and FLAC have no bext and are unchanged.
Fixes#1296.
* clap-host tests: scan a freshly created private directory for the garbage-file test
The test reused a PID-named directory under the shared temp dir after ignoring a
failed cleanup, then loaded every .clap candidate in it. Another local account
could pre-create that directory and plant a native library that the test runner
would load. Create the directory exclusively (mode 0700 on Unix) and fail
instead of reusing one that already exists.
* clap-host tests: create the garbage-file test directory beside the test binary, not under TMPDIR
An integration test creates send C while send A's window is open: the window moves to the new send
with its fader on screen, and undoing the send closes it. send_route_menu now opens the window only
after the send command succeeds, without the extra bool, and its comment says reassignment leaves
the window alone.
Picking a destination for an empty send slot, or making one with new bus..., now opens that send's Send window, as in Pro Tools. Moving an existing send to another bus leaves the window as it was.