256 MiB per line times 16 connections let a peer make the app buffer about
4 GiB. 16 MiB would break a legitimate request: mix.insert_state carries a
plugin's state as base64 inline, and the engine accepts states of up to
b64::MAX_DECODED_BYTES (64 MiB), about 85.3 MiB of base64. Nothing else sent
to the app is that large (screenshots are replies, which this doesn't bound).
So the cap is that base64 length plus 1 MiB for the JSON around it, derived
from the same constant so the two stay in step; a compile-time check keeps it
under 96 MiB, about 1.5 GiB across all 16 connections.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
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.
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>
Setup > Playback Engine now chooses the audio system (cpal host), output
and input device, device sample rate and buffer size, kept per machine
with the UI preferences and applied by reopening the devices (refused
while recording; the transport position is kept). ui.audio_device does
the same for agents and lists every system with its devices, channels,
rates and buffer range.
Player and recorder open the chosen device, fall back to the defaults
for anything unavailable (unplugged device, unsupported rate), retry
with the device's default buffer when the chosen one is refused, and
convert 32/16-bit integer devices from and to the f32 mix through
scratch buffers allocated up front (no allocation in the callbacks).
ASIO is behind an opt-in 'asio' feature (playback and app): building it
downloads Steinberg's ASIO SDK (proprietary licence or GPLv3) and needs
LLVM, so it stays off by default. Verified on Windows with an SSL 2+:
WASAPI output/input switching and buffer 256, ASIO duplex at 128.
Docs: G-01 shrunk, hardware-parity rows, roadmap log.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Built-in instruments now open a window with their parameters (driven by
mix.instrument_param). The Sampler's window adds a waveform with draggable
start and loop markers, Load Sample and Clear. Dropping an audio file on an
instrument track or a Sampler window loads it; in the browser the dropped
bytes are read and loaded through engine::io::load_sampler_bytes as one undo
step.
M4B is the same MP4/AAC container as M4A, and the decoder already reads it,
but the Open dialog, the web file picker and the file browser hid .m4b files.
Add m4b to those lists and to the extension fallback.
From #186: the Bounce dialog had no Browse button, so the file path had
to be typed, and it always proposed the home folder.
- A Browse… button opens the system save panel with an Audio filter
(WAV, AIFF, FLAC). The chosen file goes in the field, and its
extension sets the Format.
- A saved session bounces into its own folder by default; unsaved
sessions still start in the home folder.
- The desktop save panel now picks its filter by purpose (bounce or
session) and opens in the proposed file's folder when it exists, so
Save As starts there too instead of wherever the panel was last.
- The file field is 420pt wide, like the other path prompts.
convert ignored a --rate that did not parse and mapped any unknown
--bit-depth to 24-bit, then reported success. Both options are now
validated before the input is read; an invalid value prints which
option was wrong and exits non-zero without writing output. Omitted
options keep their defaults.
Keep the never-overwrite writer, and also give sources that share a path
their own files (#123). Save Copy In checks replace, then uses
save_session_copy (#124).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Request lines were read with BufRead::lines, which buffers a whole line
before anything looks at its length, and every accepted connection got its
own thread with no limit.
- Read each line with a bounded read: a line longer than MAX_LINE
(256 MiB) is refused with an error reply and the connection
closed, without buffering the rest of it.
- Serve at most MAX_CONNECTIONS (16) connections at once; further ones get
an error line and are closed. A slot is released when its connection
ends, including when its thread can't start or panics.
Request handling is otherwise unchanged. Adds tests for the line limit,
CRLF and final-line handling, an end-to-end request over loopback, and the
connection cap.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
wgpu's default DX12 compiler (Auto) loads whichever dxcompiler.dll the DLL
search path finds. SoundCraft ships none, and without a matching dxil.dll the
unsigned-DXIL warning fails wgpu's indirect-validation pipeline, the device
is lost and the app exits before the first frame. Pin the compiler to FXC,
which is part of Windows; WGPU_DX12_COMPILER still overrides it.
Fixes#185
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Close Session, New, Open, Revert, Quit, the window's close button,
dropping a session file on the window and opening one from the Workspace
window all replaced or closed the session without asking, so unsaved
edits were lost (#136). They now ask first: Save, Don't Save or Cancel.
Save on a session that has never been saved goes through Save As, then
carries on. Revert asks for confirmation instead, and Revert to Saved is
unavailable until the session has a file. A take being recorded is
stopped first, so it counts as a change. The web build has nowhere to
save, so it offers only Don't Save and Cancel. Calls from the control
channel, MCP and the CLI never ask.
Selecting, locating, zooming and scrolling no longer mark the session
modified, so the question only comes after real edits. Undoing back to
the saved state makes the session clean again. The engine compares the
document with the one last opened or saved, ignoring the selection, the
playhead and the zoom, once per revision.
Fixes#136
Save As, Save Copy In, Save As Template and Export Selected Tracks as
New Session wrote over any existing file without asking. The Save As
prompt proposes ~/<name>/<name>.scraft, so every new session suggests
~/Untitled/Untitled.scraft, and saving a second new session with the
suggested path replaced the first one.
These commands now refuse to write over an existing file unless
`replace` is true; Save, Save As and Save Copy In may still write the
open session's own file. Scripts, MCP agents and the control channel
can't replace a session by accident. While the path in the prompt names
an existing file, the prompt asks whether to replace it and its OK
button becomes Replace; Enter alone doesn't replace. Saving also
creates the session's folder, so a new session without audio can be
saved to the suggested path. The CLI's --save names its output
explicitly and keeps replacing it, like --bounce.
The app sent the window title to egui on every frame. Each viewport
command asks for another frame, so the window redrew at the display's
refresh rate with nothing happening: 35-43% of a CPU core and 13-20% of
the GPU per window on a 120 Hz Mac. While the window was covered or
minimized, eframe runs only App::logic, with no vsync to pace it, and
the same request kept one core at 100%.
The title is now sent only when it changes. The last title sent is kept
in egui's context data rather than compared with the window's, because
winit can't read a title back on X11 or Wayland.
Some work only ran because another frame always came, so an idle window
still wakes once a second, and every 30 ms while a plugin editor is
open. Autosave and the macOS menu bar's throttled refresh run on
frames, and plugin editors are native windows egui never hears from:
their edits (VST3 performEdit, CLAP output events) and main-thread
requests are serviced on frames. egui takes its predicted frame time
off every delay, so it is added back. The menu bar can now trail a
shortcut by up to a second instead of 250 ms.
Playback, falling meters, pending screenshots and scripted input ask
for the next frame from ui() instead of logic(). eframe skips ui() for
a covered or minimized window, so a hidden window runs on the heartbeat
instead of spinning, and egui redraws it as soon as it shows again.
Autosave was timed by egui's frame clock, which stands still while the
window is hidden, so a modified session wasn't autosaved until the
window showed again. It is timed by the wall clock now. Only the
desktop app sets an autosave folder, so the web build never reads it.
Tests check that an unchanged title sends no command, that only a shown
window animates while a hidden one wakes once a second, and that a
hidden window still autosaves.
Measured on a 120 Hz MacBook with a release build, the demo session and
audio off: an idle Edit window went from 35-43% CPU and 13-20% GPU to
1.5% and 0.4% (the insertion point blinks), the Mix window to 0.2%, and
a covered window, idle or playing, from ~100% CPU to 0%.
The window opened at 1600x1000 on every launch, and the interface scale
set in UI Customization reset to 100%. The app now keeps the window's
size, whether it was maximized and the scale in its preferences
(ui.json, saved on quit) and restores them at launch. The size is kept
in unscaled points, so a scaled interface doesn't change the window's
size from one launch to the next. The position is left to the system,
so a disconnected monitor can't leave the window off screen. A window
too big for the screen it opens on (sized on a larger monitor) opens
maximized instead and keeps its saved size for that monitor. Runs with
SOUNDCRAFT_NO_PREFS still start at the defaults.