Commit Graph
838 Commits
Author SHA1 Message Date
Brandon Thomas d85889aafa Merge pull request #312 from lunar-me/fix-docs/issue-007
docs: fix grammar issue in docs/target-app-parity.md
2026-10-13 04:28:03 -04:00
Brandon Thomas 63717328c5 Merge pull request #311 from lunar-me/fix-docs/issue-002
docs: fix grammar issue in README.md
2026-10-13 04:27:58 -04:00
Brandon Thomas 18579f546c Merge pull request #295 from marcoarnulfo/fix/pan-automation-bounce
fix(mix): pan automation reads the same in playback and bounce
2026-10-13 04:27:50 -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 db9ea775fc Merge pull request #287 from marcoarnulfo/fix/midi-backspace-rename
fix(midi-editor): Backspace in a text field leaves the selected notes alone (#223)
2026-10-13 04:27:30 -04:00
Brandon Thomas 8c33cefe1c Merge pull request #286 from marcoarnulfo/fix/modulation-feedback-tail
fix(dsp): chorus and flanger tails follow their feedback (#242)
2026-10-13 04:27:23 -04:00
Brandon Thomas 5bb3e7b3b6 Merge pull request #285 from marcoarnulfo/fix/pitch-shift-full-range
fix(dsp): pitch_shift reaches ±48 semitones (#243)
2026-10-13 04:27:18 -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 Thomas 741010d50c Merge pull request #281 from marcoarnulfo/fix/h264-param-set-overflow
fix(video): H.264 parameter sets check their arithmetic before range checks (#251, #252)
2026-10-13 04:27:06 -04:00
Brandon Thomas 3726c57d78 Merge pull request #280 from marcoarnulfo/fix/eq-discrete-redesign
fix(eq): switches and choices redesign the filter on a running EQ (#239)
2026-10-13 04:27:02 -04:00
Brandon Thomas e5787ab7ac Merge pull request #279 from marcoarnulfo/fix/midi-quantize-grid
fix(midi-editor): Quantize snaps to the grid the editor shows (#226)
2026-10-13 04:26:56 -04:00
Brandon Thomas f146bc359f Merge pull request #278 from marcoarnulfo/fix/pencil-plugin-range
fix(edit-window): Pencil clicks on plugin lanes use the plugin's range (#224)
2026-10-13 04:26:53 -04:00
Brandon Thomas 12224df8f5 Merge pull request #277 from marcoarnulfo/fix/hide-floating-automation
fix(window): Hide All Floating Windows also hides Automation (#253)
2026-10-13 04:26:49 -04:00
Brandon Thomas baf102e13f Merge pull request #276 from marcoarnulfo/fix/filmstrip-clip-start
fix(video-universe): filmstrip thumbnails use the clip's own movie time (#225)
2026-10-13 04:26:45 -04:00
Brandon Thomas bd7e8f271f Merge pull request #275 from marcoarnulfo/fix/video-selection-midi-target
fix(midi-editor): a selected video clip is not a MIDI target (#258)
2026-10-13 04:26:42 -04:00
Brandon Thomas b8e206273c Merge pull request #272 from marcoarnulfo/fix/midi-front-keeps-editor
fix(window): keep the MIDI Editor open on Bring to Front (#254)
2026-10-13 04:26:37 -04:00
Brandon Thomas 5c1f9d7dc8 Merge pull request #271 from marcoarnulfo/fix/command-filter-ui-rows
fix(control): apply the engine.commands filter to UI commands (#255)
2026-10-13 04:26:33 -04:00
Brandon Thomas d14edcd51c Merge pull request #270 from marcoarnulfo/fix/track-vca-null
fix(track): vca: null clears a VCA assignment, as documented (#237)
2026-10-13 04:26:28 -04:00
Brandon Thomas 06a4b21663 Merge pull request #269 from marcoarnulfo/fix/pitch-detect-large-rate
fix(dsp): a huge finite sample rate is no pitch, not an overflow (#241)
2026-10-13 04:25:54 -04:00
Brandon ThomasandClaude Opus 5.5 5183bc4dba rustfmt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 04:22:21 -04:00
Brandon ThomasandClaude Opus 5.5 ad8beac9b4 Look up the MIDI track offset without allocating on the audio thread
schedule_notes ran format!() every block for every instrument track; scan
the edit values by prefix instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 04:21:20 -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
Marco Arnulfo 40c5e9b42a VST3: hand host-set parameter values to the controller before saving state (fixes #257)
Plugin::set_param only queues the value for the processor, so a parameter that lives in the controller alone (no processor state) was saved with the controller's old value and came back wrong after load_state. set_param may run on the audio thread, so it now just marks the value; save_state, which already calls into the controller, pushes the marked values with setParamNormalized before taking the snapshot.
2026-10-13 07:06:22 +02:00
grovebit f188b66209 Leave controller events before a clip's start silent
A MIDI clip whose start was trimmed may keep the events before its new
start at negative ticks, hidden until the start is trimmed out again
(midi-clip-trim does this). The controller loop clamped every event's
tick to 0, so a hidden sustain pedal or pitch bend played at the clip
start, and the chase after a seek counted hidden values as the state
in effect.

Both loops now skip events before tick 0. Nothing changes for clips
without hidden events: notes and controllers made by the app never
start before tick 0.

Test: a sustain-pedal event at tick -480 is neither sent when the clip
starts nor chased after a seek into the clip, while a visible
controller is still chased.
2026-10-13 02:42:15 +01:00
grovebit eada1c4cfe Fit the section names in narrow Mix strips
A narrow Mix strip's sections are 56 pt wide, and INSERTS A-E (63 pt),
INSERTS F-J, SENDS A-E, SENDS F-J and INSTRUMENT (67 pt) ran into the
neighbouring strips. Pro Tools' Narrow Mix shortens them instead.

- A section name that doesn't fit with 4 pt to spare on each side uses
  Pro Tools' short form: I A-E, I F-J, S A-E, S F-J, and INST.
- The group selector says "nogrp", as Pro Tools' does, instead of
  "no gr…".
- Narrow strips leave the empty comments box without its hint, which
  wrapped as "comme / nts".

Normal strips are unchanged. tests/mix_narrow_headers.rs checks both
widths, including that no label in a narrow strip is cut short; it
fails on main.
2026-10-13 01:36:13 +01:00
grovebit c82b3887f0 Draw faders and meters more like Pro Tools
Pro Tools draws a strip's fader and meter as one unit: the meter's dB
scale sits between them, the meter spans the fader's travel, and its
bar is a solid column over dark tints of its zones. Ours had LED
stripes, no meter scale, and a meter taller than the fader's travel.

- Meters: a solid bar that lights green, yellow and red over dark tints
  of those zones (fainter in light wells), ruled at the scale's marks,
  with a clip light above the bar and a 2 pt peak hold in its zone's
  colour. This is the shared meter, so the Edit window, toolbar and
  transport meters change too.
- Mix strips: the meter runs the fader's travel, 0 dBFS level with
  +12, and its 0-60 dB scale sits between the fader and the meter.
  Narrow strips show the fader's ticks without numbers, as Pro Tools'
  Narrow Mix does.
- Faders: a narrower, taller cap (18 x 40 pt, 16 pt wide in narrow
  strips) with a pinched waist and a dark centre line, in a 4 pt slot.
  The scale's numbers sit beside the cap and skip marks too close to
  the one above.

tests/mix_strip_layout.rs checks that the meter scale's 0 is level
with the fader's 12 and that narrow strips draw no scale numbers; both
fail before this change.

Part of #138.
2026-10-13 01:33:28 +01:00
grovebit 7be845abd1 Transient detection: find quiet and legato onsets, place them on the attack
detect_transients (Tab to Transient, Separate at Transients, Beat Detective)
missed most onsets that weren't the loudest around, and put notes that start
over a ringing note 25-35 ms early, in the previous note's tail. Its
threshold was a share of the loudest flux in the analysed audio, so quiet
hits vanished next to loud ones and the result changed with where the
analysis started. It placed each onset on the first sample reaching a
quarter of the frame's peak, which a ringing note already does.

- Vibrato-suppressed spectral flux: 21 ms windows 5.3 ms apart, log
  magnitudes on 24 bands per octave from 30 Hz to 20 kHz, each band compared
  with the loudest of its neighbours in the frame before, so vibrato and
  chorus don't count. Channels are analysed as the sum of their energies,
  so stereo content that cancels in mono still counts.
- A peak must be the highest within 30 ms and exceed the mean flux from
  100 ms before to 70 ms after by a fixed amount (2.5 at sensitivity 0.5),
  so quiet hits count and the result doesn't depend on where the audio
  starts.
- A fast release smears energy between a note's partials, which looks like
  a rise. A detection also needs a 3 dB step in its high-passed energy, or
  total energy that grows in its frame or the next.
- Each onset goes to the step edge of the high-passed energy (the sample
  where the next 3 ms gain most over the 6 ms before), then back to where
  the energy crosses the midpoint between the two: 0.3-0.5 ms before
  percussive attacks.

On 29 synthetic cases with known onsets (drums with ghost notes; drum-synth
grooves dry and through reverb, compression and limiting; 180 bpm; flams and
claps; legato piano; plucked bass and guitar; strums; 808 kicks; hand drums;
synth bass, leads, pads and staccato notes; string swells; a vibrato pad;
quiet hits over noise and hum; stereo pairs; formant syllables), the
F-measure went from 0.658 to 0.977. It found 1398 onsets instead of 870,
with 5 false ones instead of 315; all 5 are vowels starting after a
consonant. Still missed: hats under a crash, ghost notes under a louder
hit's tail, and slow string swells.

The old offline_meters test bursts stopped dead at -37 dB, an audible click
that now counts as a transient; they ring out instead.
2026-10-13 00:46:34 +01:00
grovebit b9e4873ffa Tab to Transient: every selected track, Alt+Tab back, Shift+Tab extends
Tab found the next transient only on the first selected track, analysed
everything from the cursor to the end of the track on every press, and
Alt+Tab, Shift+Tab and Alt+Shift+Tab ignored Tab to Transient (Shift+Tab
did nothing at all). The toolbar button used the Selector tool's icon.

- A new command, edit.tab {back?, extend?}, does what the Tab keys do, as in
  Pro Tools: Tab and Alt+Tab (Option+Tab on a Mac, Ctrl+Tab on Windows and
  Linux) move the insertion point to the next or previous clip start, end or
  sync point on the selected tracks; Shift+Tab and Alt+Shift+Tab
  (Ctrl+Shift+Tab) extend the selection. Alt+Shift+Tab pulls the selection
  end back while a stop lies inside the selection, then grows its start.
- With Tab to Transient on, the stops also include the transients on every
  selected audio track and the note-ons and note-offs on MIDI and Instrument
  tracks. A transient within 10 ms of a clip boundary or note is that stop,
  not a second one.
- Transients are detected in fixed 10-second blocks of each clip, so Tab and
  Alt+Tab always find the same ones and a long clip costs one block per
  press. Analysed blocks are cached while their clips, clip effects and audio
  are unchanged: 0.5 ms a press across the demo's five audio tracks, 43 ms
  without the cache.
- The Tab to Transient button gets its own icon, an arrow into a transient.

Cmd+Alt+Tab still toggles Tab to Transient. On Windows and Linux the desktop
takes Ctrl+Alt+Tab, and Pro Tools' Ctrl+Alt+T is Thin Automation here too, so
the button and the Options menu are the way there.
2026-10-12 23:51:18 +01:00
grovebit 8f8884ec05 Refuse to replace the session while bouncing
A bounce keeps its own snapshot of the session, decoded audio included.
The progress window blocks the mouse, but shortcuts, the macOS menu bar,
dropped files and the control channel still reach SoundApp::run, so a
second session could be opened beside the one bouncing; two large ones
could exhaust memory, which aborts the app.

New, Open, Close and Revert now fail with "Finish or cancel the bounce
first" while a bounce runs, and so do sessions opened or dropped in the
browser.
2026-10-12 23:47:24 +01:00
grovebit d19d44cd1b Cap bogus plugin latency so offline renders can't stall
CLAP and VST3 plugins report their latency unchecked (up to u32::MAX
samples), and an offline render skips that many samples before its first
block, inside one call. A plugin reporting garbage made a bounce render
about 25 hours of audio before producing anything, with the UI stuck in
that call: no progress, no Cancel. Freeze and Commit stalled the same
way.

- A strip reports at most 2^20 samples of latency (about 22 s at 48 kHz),
  the most delay compensation could align anyway (MAX_LATENCY, shared
  with the delay lines). That holds for the latency an aux's inserts
  report before the aux is processed, too.
- RangeRender::next_block renders one block per call, returning no frames
  while it skips the latency, so a bounce yields to the UI there too.
2026-10-12 23:47:24 +01:00
grovebit 52e19a871a Keep the app responsive while bouncing
Bounce Mix rendered the whole file inside one UI frame: the window stopped
drawing and nothing could cancel the bounce until the file was written,
which takes minutes for long or plugin-heavy sessions.

- engine: io::Bounce renders a slice at a time (step) and reports its
  progress; dropping an unfinished one cancels it, removing the partial
  file and keeping any older one. cmd::bounce_job builds it from
  file.bounce_mix's params; the command and stems still run it to
  completion.
- ui: the Bounce dialog starts the job and logic() renders it for 30 ms
  per call (32 blocks in the browser, where Instant panics), requesting
  another call each time, so it carries on while the window is hidden
  (eframe then runs logic alone). A modal shows the file, a progress bar
  and Cancel (or Esc); the status line reports the result, and the Task
  Manager lists the running bounce.
- It stays on the UI thread because hosted plugins must be created there.
2026-10-12 23:47:00 +01:00
grovebit 5755795663 Stream bounces to disk instead of rendering them in memory
A bounce rendered the whole mix (or bus) into memory and then encoded a
second copy, and the render silently stopped at 2^29 samples: about 93
minutes of stereo at 48 kHz, 31 of 5.1, 15 of 7.1.4. Longer bounces came
out cut short while file.bounce_mix still reported the requested length.

- audio-io: FileEncoder writes WAV/BWF (RF64 above 4 GiB), AIFF and FLAC
  block by block into any Write + Seek, byte for byte what encode writes
  for the whole buffer. check_room fails up front when an AIFF (4 GiB) or
  a FLAC (2^36 frames) can't hold the bounce. AIFF and FLAC encode run on
  the same writers; their output is unchanged.
- mix: RangeRender renders the main mix or one bus over a range of any
  length, a block at a time; render_range and render_bus (in memory,
  still capped) are built on it.
- engine: io::Bounce streams the main mix or a bus, or its stereo
  fold-down, into "<name>.part" next to the target, syncs it and renames
  it into place, so a failed bounce leaves no partial file and keeps the
  old one. In the browser it encodes into memory (failing instead of
  aborting when memory runs out) and delivers the file as before.
  Normalize renders twice (peak, then the scaled write); levels are
  measured as blocks are written. Stems stream the same way and share the
  4-hour limit.

A 100-minute bounce of the demo session now writes all 288,000,000
frames with 83 MB peak memory.
2026-10-12 23:46:13 +01:00
grovebit 7f9b629964 Draw each take in the Edit window while it records
Pro Tools draws a take's waveform as it records; ours showed nothing
until Stop. Each record-enabled track now shows its take growing as a
red clip with its waveform, drawn from the take's live summary through
the same column grid and channel bands as clips (Peak view, as Pro Tools
always shows while recording).

- The take writer keeps, per take channel, the (min, max) of every 128
  frames it writes (at most 2^22 blocks, three hours at 48 kHz; the file
  records on regardless). Recorder::live_take hands the Edit window what
  a track's take holds so far, while the writer runs. The blocks sit
  behind a mutex the writer thread and the UI lock briefly; the input
  callback is untouched.
- io::live_span places the take as Stop will: from the record start,
  only the pass under way when loop recording, and within the punch
  range.

Tests: the take writer's summary (record.rs), live_span with loop and
punch (io.rs) and draw_live (tests/waveform.rs).
2026-10-12 23:43:58 +01:00
grovebit 1cd0233f49 Open a MIDI clip in the MIDI Editor by double-clicking it with the Grabber
Double-clicking a MIDI clip in the Edit window only selected it, so the
MIDI Editor had to be opened from Window › MIDI Editor or the dock tabs.
In Pro Tools a Grabber double-click on a MIDI clip opens it in a MIDI
Editor (the "Double-Clicking a MIDI Clip Opens" MIDI preference, which
defaults to an editor); SoundCraft has one, docked, editor.

- A double-click with the Grabber on a MIDI or instrument clip selects
  the clip, as before, and opens the docked MIDI Editor on it. An editor
  that's already open stays open and switches to the clip.
- The Smart Tool follows its zones: its lower half is the Grabber and
  opens the editor; its upper half is the Selector, where a double-click
  still only selects the clip, as in Pro Tools.
- Audio clips are unchanged: a double-click selects them.

Tests: a Grabber double-click opens the editor on the clip; with the
Smart Tool, the upper half selects without opening it and the lower half
opens it; an audio clip leaves the editor closed. The first two fail on
main.
2026-10-12 23:42:58 +01:00
grovebit e44c49cada Record NaN and infinite input samples as silence
A device or driver that sends NaN or infinite samples got them written
into the take, where they poison whatever the clip is mixed with, and
into the input monitor. Both paths now turn them into silence before
they are queued, so a take keeps its length and timing.
2026-10-12 23:33:48 +01: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 0b9ead430f Stop the transport when the audio output device goes away
Losing the output device only reached the log: the transport kept
"playing" a frozen playhead, and a recording ran on with nobody told.

The output now reports trouble once: the device was disconnected, or it
stopped asking for audio for two seconds while playing (not checked in
the web build, which has no clock for it), or the engine hit a bug as
before. The app stops the transport, keeps a take in progress, and says
what happened and how to reconnect.
2026-10-12 23:33:48 +01:00
grovebit ddb9c9543d Stop recording and keep the take when the input or the disk fails
A failing input only reached the log. When the interface was unplugged
mid-take (on macOS its callbacks simply stop), when writing a take
failed (a full disk), or when the input callback panicked, recording
went on with the transport running and the take silently ended early.
A panic in the callback could also take the input stream down for good.

The recorder now reports trouble: the device was disconnected, it sent
nothing for two seconds while recording, its callback hit a bug (caught,
after which it does nothing more), or writing a take failed. The app
stops recording at once, keeps the take up to that point and says what
happened and what to do. Input lost to a slow disk is reported while
recording instead of only at Stop. Reconnect audio device now reopens
the input as well as the output, so monitoring works again afterwards.
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 98d543f0d8 Go silent instead of crashing when the audio engine hits a bug
Nothing caught a panic in the audio output callback. On macOS it
unwinds into CoreAudio's thread, which most likely aborts the app; on
Windows and Linux the stream thread dies while the transport keeps
showing playback with no sound. A panic in the silent clock that runs
without an audio device stopped the clock for good.

Each block is now filled under a guard. A panic makes that block and
every later one silent and marks the engine failed. The app then stops
the transport, keeping what was recorded, and says to reconnect the
audio device or restart.
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
woodgrain816andClaude Opus 5.5 1a49fc70fb File › Open Recent: the last 10 sessions, newest first
Pro Tools has File › Open Recent; SoundCraft had no way back to a session but
browsing for it. The list (UiState.recent_sessions) records the engine's
session path whenever it changes, so opening or saving by any route (menu,
control channel, MCP, a dropped file) is covered by one rule.

- Under File › Open; each entry is "name — folder", full path on hover.
- A session that has moved or been deleted is greyed out "(missing)" instead
  of failing on click; existence is re-checked at most every 3 s while the menu
  is open, since a network share costs a round trip per file.
- Opening goes through the dialog layer, so unsaved changes are asked about.
- Clear Recent empties it.
- The desktop host saves ui.json as soon as the list changes, not only on a
  clean exit: the list matters most after a crash.
- An older ui.json without the field still loads (serde default).

The macOS system menu bar (native_menu) does not show it yet; the egui menu
bar on Windows and Linux does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 17:31:22 -05:00
grovebit 6fd17d7c0e Keep shortcuts working after Tab
With no widget focused, egui answers Tab by giving keyboard focus to the
first widget in the window, and Shift+Tab to the last one. shortcuts::handle
ignores every key while a widget has focus, so after Tab (to the next clip
boundary) R/T zoom and every other shortcut did nothing until a click
elsewhere or Escape. Space and Enter clicked the focused widget instead: on
macOS that is the toolbar's Shuffle button, so Space switched the session to
Shuffle; on Windows and Linux it opened the SoundCraft menu.

While the shortcuts own the keyboard (nothing focused, no dialog open),
handle() now cancels egui's focus move for the frame. Tab still moves between
fields once one has focus, and inside dialogs.
2026-10-12 23:16:11 +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