recording.json names the take files whose headers recovery rewrites. Before
writing, check that each is a regular file (not a symlink) directly in the
Audio Files folder a recording of that session writes into (next to the
session file, or in its folder under the autosave root) and that it starts
with the float WAV header the recorder writes. Anything else is left
untouched, logged and listed with the media recovery couldn't load.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
clip.unloop found loop copies only by comparing content of back-to-back
clips. As soon as the source clip's notes were edited, its content no
longer matched the copies and unloop answered "the selection is not a
looped clip" for the very loop that clip.loop had just created.
clip.loop now tags the origin and its copies with a shared `loop_id`
(new optional Clip field, serde default so old sessions load), and
unloop treats equal-length adjacent clips with the same loop_id as one
loop. The content comparison stays as a fallback for loops without a
tag. unloop with mode=flatten clears the tag so the copies become
ordinary independent clips.
Propagating edits from the source to the copies is not part of this
change.
Takes stream to disk while recording, but they only join the session at
Stop. After a crash the files were on disk with nothing pointing at
them and no record of where they started, and the header of each was up
to a second behind its audio.
When recording starts, its plan (each track's take file, where the
takes start, the loop length and punch range) is autosaved with the
session, and every autosave until Stop keeps it there. Stop places the
takes from that same plan. Recovering a folder that still holds a plan
first repairs each take's header to cover all of its audio, then places
the takes as Stop would have, skipping any the session already has. The
recovery window says when there's an unfinished recording, and such a
folder is kept even if its session file was saved later.
Opening, creating, closing or reverting a session in the middle of a
take dropped the recorder: the take was written to disk but never added
to any session. Quitting mid-take did the same, and on_exit didn't
finish the take either.
New, Open, Close, Revert and Quit now wait for the take: the engine
refuses them with "stop recording first", and their menu items are
greyed out while recording, as is Recover in the recovery window. Any
exit that still happens mid-take (closing the window, the system
quitting the app, a remote quit) finishes the take first and keeps the
session, take included, in the autosave folder for the next launch.
Autosave ran every two minutes and not at all while recording, so a
crash could lose minutes of work. It also wrote on the UI thread: the
first autosave after a recording copied the take, which could freeze
the window for seconds.
The open session now goes to a writer thread after every change,
recording or not. The first change after a pause is written at once;
while changes keep coming, as in a fader drag, the newest is written at
most once a second. The UI waits for the writer only where it needs the
result: after a caught panic, when recovering and when quitting. A bug
in a write is caught and reported, and the next change tries again.
Plugins' own state still reaches the session every two minutes and not
while recording, since plugins save it on the audio thread.
Nothing caught a panic outside a command: one in the interface's frame
code ended the process in that frame (exit code 101), losing every
unsaved change, and the log got a single line with no backtrace.
Each frame's logic and drawing now run guarded. A panic there autosaves
the session (once per change, so a bug that repeats every frame doesn't
flood the disk), says so in the status line and lets the app carry on.
If the app goes down after all, the autosave is offered at the next
launch. The panic hook now logs a backtrace.
Autosave wrote every session called "Untitled" to the same
Autosave/Untitled folder and reused audio files there by source id, so
a second untitled session, or a take recorded after an undo, could be
autosaved with another take's audio. Its first run also copied all of
the session's audio again. Nothing ever offered an autosave back
(latest_autosave had no callers), so after a crash the work could only
be found by digging in the settings folder.
Each open session now autosaves to a folder of its own, locked while
the session is open. Audio already saved with the session is referenced
where it is; audio that exists only in memory is copied once per
recording or render. Saving, closing the session, opening another and
quitting with SoundCraft's Quit delete the folder. A crash, or an exit
that skips Quit with unsaved changes, leaves it behind, after one last
autosave when SoundCraft gets the chance. At the next launch,
SoundCraft offers each kept folder, newest first, to recover or
discard. Recovered work opens as an unsaved version of the session it
came from, so Save writes it back there; unsaved changes in the session
it replaces are kept the same way. Folders older than their session
file are dropped.
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>
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>
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.
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
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.
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.
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.
saved_audio_is_trusted_only_in_the_folder_it_was_saved_in compared path
strings. On Windows the engine reports <session folder>\Audio Files/x.wav
while the test expected <folder>/Audio Files/x.wav, so the test failed
there although the two name the same file. The expected value is now the
session folder joined with the stored relative path, on every platform.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
event.midi_track_offsets stored midi.offset.<track> but the note
scheduler never read it, so the render was unchanged. schedule_notes
now looks that many samples back into the clips, which plays the track
exactly as if its MIDI had been moved by the offset. The scheduler is
shared by offline bounce and realtime playback.
A write pass added a point every 20 ms of playback and never removed
the old points between them, so the old automation survived between
the new points and the level jumped between the two rides. A covered
window, which runs only once a second since #192, wrote one point a
second and kept almost all of the old ride.
automation.set_point takes an optional `from`, and the points between
`from` and `at` are removed first. Each point of a pass gives the
previous one, so it replaces everything since then however far apart
the passes run, and Stop writes a last point where playback stopped.
A locate or a restart while playing starts afresh, so a jump never
erases what it skipped.
The test writes over a dense old ride with passes a quarter second
apart, jumps twice and stops, then checks each stretch. Removing any
of the four changes fails it.
Setup > Current Feet+Frames Position stored its origin in
feet_frames.start but nothing read it, so every Feet+Frames reading
still counted from sample 0. format_position/parse_position now take
the origin alongside the timecode start, and Session::format_position
and Session::parse_position pass both, so the counters, rulers,
session.format_time and typed positions share one reference.
Trim to File Boundaries and Trim to Fill Selection extended a clip within
its source without looking at the clips around it, leaving two unfaded
clips over the same samples. Both now go through one helper that caps the
extension at the end of the previous clip and the start of the next one on
the same playlist, the way the other commands that grow an existing clip
do (Paste Merge MIDI, step input). An existing crossfade overlap is kept.