Commit Graph
162 Commits
Author SHA1 Message Date
Brandon ThomasandClaude Opus 5.5 3cc0dc77cb Fix build after merging main into #368
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 04:56:26 -04:00
Brandon Thomas 46399aa729 Merge main into #368 2026-10-13 04:50:04 -04:00
Brandon Thomas 5495378d7a Merge pull request #309 from isaac-levine/mcp-automation-roots
MCP: confine file access with --automation-read-root and --automation-write-root
2026-10-13 04:43:44 -04:00
Brandon Thomas 9c7e5018b3 Merge pull request #333 from grovebit/strip-silence-long
Stop Strip Silence deleting clips late in long selections
2026-10-13 04:43:41 -04:00
Brandon Thomas 6c836b4389 Merge pull request #308 from grovebit/automation-overwrite
Automation passes replace the automation they play over, in one undo step
2026-10-13 04:43:21 -04:00
Brandon Thomas b764e299bc Merge pull request #326 from grovebit/long-renders
Refuse renders too long to finish instead of cutting the audio short
2026-10-13 04:42:56 -04:00
Brandon Thomas 7347e98e79 Merge pull request #363 from grovebit/more-shortcuts
Pro Tools keys for nudging, Grid and Nudge values, playlists and windows
2026-10-13 04:42:25 -04:00
Brandon Thomas bdff15defb Merge pull request #387 from itrycodinganditsbad/fix-250
Keep later meter changes on their bars when an earlier meter is edited
2026-10-13 04:32:53 -04:00
Brandon Thomas aa91e47073 Merge pull request #303 from marcoarnulfo/fix/restore-last-selection
Restore Last Selection brings back the previous selection
2026-10-13 04:32:49 -04:00
Brandon Thomas ba4c234344 Merge main into #387
# Conflicts:
#	crates/engine/src/cmd/event.rs
2026-10-13 04:30:17 -04:00
Brandon Thomas 0f6bd8a573 Merge main into #303
# Conflicts:
#	crates/engine/src/cmd/edit.rs
2026-10-13 04:30:17 -04:00
Brandon Thomas 52294bd4a3 Merge main into #291
# Conflicts:
#	crates/engine/src/cmd/event.rs
2026-10-13 04:30:17 -04:00
Brandon Thomas 6dfa00590b Merge pull request #304 from marcoarnulfo/fix/midi-track-offsets
Apply MIDI track offsets when scheduling notes
2026-10-13 04:29:40 -04:00
Brandon Thomas 6bab8365e6 Merge pull request #289 from marcoarnulfo/fix/clear-trim-automation
fix(track): Clear Trim Automation removes the trim lane (#233)
2026-10-13 04:29:36 -04:00
Brandon Thomas cd1ddff0c0 Merge pull request #324 from iannitram/fix/gate-on-windows-and-rust-1.95
Make cargo xtask ci pass on Windows, and on Rust 1.95
2026-10-13 04:29:32 -04:00
Brandon Thomas e59f079a02 Merge pull request #342 from mroxso/t3code/76964692
fix(mix): mix.insert_params rejects unknown parameter names
2026-10-13 04:29:23 -04:00
Brandon Thomas b0339a418f Merge pull request #401 from Claudio911015/fix/273-clip-loop-unloop-consistency
Make clip.loop / clip.unloop self-consistent (loop_id tag)
2026-10-13 04:29:19 -04:00
Brandon Thomas a307f767ba Merge pull request #212 from itrycodinganditsbad/consolidate-selection
Consolidate covers the whole edit selection (#164)
2026-10-13 04:28:45 -04:00
Brandon ThomasandClaude Opus 5.5 dac86131ee Recovery repairs only take files in the folders recordings write to
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>
2026-10-13 04:28:36 -04:00
Brandon Thomas a89c0c4db5 Merge pull request #379 from webdevtodayjason/trim-to-selection-order
Keep clips in order after Trim to Selection
2026-10-13 04:28:26 -04:00
Brandon Thomas 54254639cc Merge pull request #358 from sajibsrs/fix/282-new-midi-clip-menu
Add Clip › New MIDI Clip so an empty MIDI track can get a clip (#282)
2026-10-13 04:28:22 -04:00
Brandon Thomas 3c53d25413 Merge pull request #294 from marcoarnulfo/fix/feet-frames-reference
Apply the Feet+Frames reference to counters and typed positions
2026-10-13 04:27:47 -04:00
Brandon Thomas 3ef50746c8 Merge pull request #292 from marcoarnulfo/fix/trim-no-overlap
fix(edit): trims stop at the neighbouring clips instead of overlapping them (#228, #229)
2026-10-13 04:27:43 -04:00
Brandon Thomas 704ebc3c76 Merge pull request #288 from marcoarnulfo/fix/clip-gain-cut-clipboard
fix(edit): cut clip gain keeps the removed gain for paste (#230)
2026-10-13 04:27:37 -04:00
Brandon Thomas f58da8fe8a Merge pull request #284 from marcoarnulfo/fix/elastic-properties-ratio
fix(clip): Elastic Properties stretches the clip by the requested ratio (#227)
2026-10-13 04:27:14 -04:00
Brandon Thomas 99dbd4d628 Merge pull request #283 from marcoarnulfo/fix/marker-edit-name
fix(markers): markers.edit applies the documented name (#236)
2026-10-13 04:27:10 -04:00
Brandon ThomasandClaude Opus 5.5 165fa98a90 Count tracks, not lanes, in track.clear_trim_automation's result
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 04:21:20 -04:00
Claudio bee999584f Make clip.unloop recognise loops made by clip.loop, even after edits
clip.unloop found loop copies only by comparing content of back-to-back
clips. As soon as the source clip's notes were edited, its content no
longer matched the copies and unloop answered "the selection is not a
looped clip" for the very loop that clip.loop had just created.

clip.loop now tags the origin and its copies with a shared `loop_id`
(new optional Clip field, serde default so old sessions load), and
unloop treats equal-length adjacent clips with the same loop_id as one
loop. The content comparison stays as a fallback for loops without a
tag. unloop with mode=flatten clears the tag so the copies become
ordinary independent clips.

Propagating edits from the source to the copies is not part of this
change.
2026-10-12 23:54:33 -06:00
grovebit 30122bcea1 Recover the takes of a recording a crash interrupted
Takes stream to disk while recording, but they only join the session at
Stop. After a crash the files were on disk with nothing pointing at
them and no record of where they started, and the header of each was up
to a second behind its audio.

When recording starts, its plan (each track's take file, where the
takes start, the loop length and punch range) is autosaved with the
session, and every autosave until Stop keeps it there. Stop places the
takes from that same plan. Recovering a folder that still holds a plan
first repairs each take's header to cover all of its audio, then places
the takes as Stop would have, skipping any the session already has. The
recovery window says when there's an unfinished recording, and such a
folder is kept even if its session file was saved later.
2026-10-12 23:33:48 +01:00
grovebit 62cb92c4df Never replace or quit a session while it records
Opening, creating, closing or reverting a session in the middle of a
take dropped the recorder: the take was written to disk but never added
to any session. Quitting mid-take did the same, and on_exit didn't
finish the take either.

New, Open, Close, Revert and Quit now wait for the take: the engine
refuses them with "stop recording first", and their menu items are
greyed out while recording, as is Recover in the recovery window. Any
exit that still happens mid-take (closing the window, the system
quitting the app, a remote quit) finishes the take first and keeps the
session, take included, in the autosave folder for the next launch.
2026-10-12 23:33:48 +01:00
grovebit 334cf40506 Autosave after every change, on a thread of its own
Autosave ran every two minutes and not at all while recording, so a
crash could lose minutes of work. It also wrote on the UI thread: the
first autosave after a recording copied the take, which could freeze
the window for seconds.

The open session now goes to a writer thread after every change,
recording or not. The first change after a pause is written at once;
while changes keep coming, as in a fader drag, the newest is written at
most once a second. The UI waits for the writer only where it needs the
result: after a caught panic, when recovering and when quitting. A bug
in a write is caught and reported, and the next change tries again.
Plugins' own state still reaches the session every two minutes and not
while recording, since plugins save it on the audio thread.
2026-10-12 23:32:47 +01:00
grovebit 5fff2e8c78 Keep working when the interface hits a bug
Nothing caught a panic outside a command: one in the interface's frame
code ended the process in that frame (exit code 101), losing every
unsaved change, and the log got a single line with no backtrace.

Each frame's logic and drawing now run guarded. A panic there autosaves
the session (once per change, so a bug that repeats every frame doesn't
flood the disk), says so in the status line and lets the app carry on.
If the app goes down after all, the autosave is offered at the next
launch. The panic hook now logs a backtrace.
2026-10-12 23:32:47 +01:00
grovebit b5ea465274 Autosave each session to its own folder and offer unsaved work at launch
Autosave wrote every session called "Untitled" to the same
Autosave/Untitled folder and reused audio files there by source id, so
a second untitled session, or a take recorded after an undo, could be
autosaved with another take's audio. Its first run also copied all of
the session's audio again. Nothing ever offered an autosave back
(latest_autosave had no callers), so after a crash the work could only
be found by digging in the settings folder.

Each open session now autosaves to a folder of its own, locked while
the session is open. Audio already saved with the session is referenced
where it is; audio that exists only in memory is copied once per
recording or render. Saving, closing the session, opening another and
quitting with SoundCraft's Quit delete the folder. A crash, or an exit
that skips Quit with unsaved changes, leaves it behind, after one last
autosave when SoundCraft gets the chance. At the next launch,
SoundCraft offers each kept folder, newest first, to recover or
discard. Recovered work opens as an unsaved version of the session it
came from, so Save writes it back there; unsaved changes in the session
it replaces are kept the same way. Folders older than their session
file are dropped.
2026-10-12 23:32:47 +01:00
Fabio LussiandClaude Opus 5.5 3432b52373 Keep later meter changes on their bars when an earlier meter is edited
set_meter and remove_meter now re-place every later meter event at the
bar number it started before the edit, so meter changes stay on bar
lines and bar|beat positions round-trip. Clips, notes and tempo events
keep their positions.

Fixes #250

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 23:52:50 +02:00
webdevtodayjason 950f3e7085 Keep clips in order after Trim to Selection 2026-10-12 16:09:01 -05: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
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
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 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 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
Ian MartinandClaude Opus 5.5 b2f6361265 engine: build the expected missing-file path the way the engine does
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>
2026-10-12 10:34:41 -06:00
Isaac Levine 29497296a1 Engine: check automation read and write roots on every path a command reads or writes 2026-10-12 10:50:04 -04:00
Marco Arnulfo 4bacfb13d5 Apply MIDI track offsets when scheduling notes (fixes #234)
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.
2026-10-12 16:48:50 +02:00
Marco Arnulfo d5f069417e Keep the last selection out of the document so selecting leaves the session clean 2026-10-12 16:47:56 +02:00
grovebit 4b98064e46 Automation passes replace the automation they play over
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.
2026-10-12 15:47:08 +01:00
Marco Arnulfo 1f7a4ab3e4 Restore Last Selection brings back the previous selection (fixes #231) 2026-10-12 16:42:19 +02:00
Marco Arnulfo 5e7ca6de22 Apply the Feet+Frames reference to counters and typed positions (fixes #235)
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.
2026-10-12 16:36:18 +02:00
Marco Arnulfo 4e14c980a4 fix(edit): trims stop at the neighbouring clips instead of overlapping them (#228, #229)
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.
2026-10-12 15:50:38 +02:00
Marco Arnulfo e5d5996f7d fix(event): Constant tempo applies only over its range (#238) 2026-10-12 15:36:40 +02:00