The web build could not reach any file: File > Open Session…, Import > Audio… and Import > MIDI…
showed a text box for a path the browser can't read, and every save or export failed with
«operation not supported on this platform», so work was lost on close.
On the web now:
- Open Session…, Import Audio… and Import MIDI… show the browser's file picker; files dropped on
the page open (sessions) or import (audio, MIDI) by extension. Files travel as bytes; imports are
one undo step each, like the import commands.
- A session is saved as ONE file, a bundle `<name>.scraftz`: a zip (stored entries) holding the
`.scraft` and its `Audio Files/` as 32-bit float WAV — the same layout as a session folder, since
the browser has no folders. `io::open_session_bytes` opens a bundle or a bare `.scraft` (its audio
is then reported missing). Bundles are hostile input: entry count and size are capped, and
declared sizes are never read past.
- Every single-file output (save, Save As Template, bounce, MIDI/score/text export, track presets)
goes through `io::write_file`: a file natively, a download in the browser via a delivery hook
(`io::set_deliver`) that only the web app installs.
- In the browser Save As asks for the bundle's name (`<name>.scraftz`) instead of a folder path.
Native builds keep writing files and session folders as before. Folder outputs (stems, clip
exports) are still not available on the web.
Pairs with #157 (Save on a never-saved session opens Save As), which the browser needs too: the
web app always starts on the unsaved demo.
New dependency: `zip` (MIT), default features off (stored entries only).
Checked: engine tests for the bundle round trip (every sample identical), same-named sources,
bare sessions and hostile bundles; `cargo xtask ci`. In Chromium on the packaged web build:
⌘⇧I picks and imports a WAV; WAV, MP3, FLAC, OGG, M4A, AIFF and MIDI import by drop; Save As →
`Midnight Groove.scraftz` downloads with the imported audio inside; that bundle reopens in a fresh
page with all tracks and no missing media, and saves again with the same content.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Cmd+S and File > Save Session on an untitled session only left "no path
yet: pass `path` (Save As)" in the status line. From a menu, the command
palette or the keyboard, Save now opens the Save Session As prompt until
the session has a file. Calls from the control channel, MCP and the CLI
still need a `path`.
The prompt suggests ~/<name>/<name>.scraft, but saving a session without
audio there failed with "No such file or directory", because only
writing audio created the session folder. Saving now creates the folder.
Fixes#144. Unless Options::zoom_with_keyboard is off, egui zooms the
whole interface on Cmd+= (or Cmd+Plus), Cmd+- and Cmd+0. It checks for
those keys at the end of every frame, after SoundCraft's shortcuts have
read them without consuming them. So every Cmd+= that switched between
the Mix and Edit windows also zoomed in by another 10 %, and Cmd+0
(Quantize to Grid) reset the interface scale. Ctrl did the same on
Windows and Linux.
Turn egui's keyboard zoom off. The Interface scale slider in Setup >
Appearance is now the only thing that sizes the interface, so it always
shows the real scale. Before, keyboard zoom changed the scale without
moving the slider, and could go past its 75-150 % range.
Dragging that slider rescaled the whole interface on every frame of the
drag. Each rescale moved the slider relative to the pointer, which
stays put on screen, so the next frame read another value under the
pointer. The scale jumped back and forth (105, 110, 105 ... 145, 120,
150, 90 % in one drag) and the slider would not stay where it was
dragged. Apply the scale once the mouse button is released. It is now
applied after the window has drawn, too, so a click, Reset or an arrow
key takes effect in the same frame instead of waiting for the next
input.
Agents that only check isError were treating partial failures as success. Also reject a non-array calls value instead of silently running nothing. Fixes#145.
Export Clips as Files had no shortcut and no menu entry, so Shift+Cmd+K,
its key in Pro Tools, did nothing (#7). It now opens the command's
folder prompt, and OK exports the selected clips there.
Trying it showed three more gaps:
- Browse… in a folder prompt opened an audio-file picker, so no folder
could be chosen. Folder prompts (Export Clips, Bounce Stems) now use a
folder picker, a new `pick_folder` host service; the web build has
none and keeps the text field.
- A successful export said nothing. The status bar now reads "Exported
N clip(s) to <folder>", and the command returns the folder.
- With nothing selected the export succeeded and wrote no files. It now
fails with "no clips selected".
The shortcut lookup moves into `command_for` so a test can check what a
key resolves to.
The command cleared every automation lane in the selection and left the clipboard empty, so the cut could not be pasted. It now uses the same copy-and-clear path as Cut Pan Automation.
engine.execute and app.run of ui.new_tracks_dialog and ui.bounce_dialog were opening modal dialogs, so a script could not create tracks or bounce without a window. Those ids now return an error unless the menu invokes them.
The bounce probe read aux latency before the aux had processed, so a lookahead compressor shifted the export and dropped audio at the end of the range.
Clip gain and clip effects renders wrote the clip as it already sounded and left the old ratio in place, so the new file was stretched a second time and its tail went silent.
Both processes rendered a unity-gain file and then cleared the clip's gain and envelope, so the result was quieter than the clip the user heard. Invert and Duplicate now leave that gain on the new clip. Processes that bake a new level still write a unity-gain clip.
Half-Speed Playback stored 0.5 on the player, and a manual Stop left that rate in place, so the next ordinary Play stayed at half speed. Stopping now resets the player to 1.0. Half-speed itself still plays at 0.5, and a pass that ends on its own still returns to full speed.
The command wrote the lane onto the whole clip and cleared points only inside the selection, so the old curve still played and the clip got quieter on both sides. The selection now holds the fader, with anchors so the surrounding curve stays put. Volume and clip-gain windows take their end value from the last sample inside the window, so a breakpoint on the next sample does not ramp the audio before it.
Export Clips as Files wrote every clip to {name}.wav, so the second clip replaced the first. A name that is already in the destination now gets a -2, -3, … suffix.
Snap to Next and Snap to Previous ignored other selected clips when looking for a destination, so a multi-clip selection piled onto the same later clip or left the gap between the selected clips in place.
The menu bar rebuilt and leaked a fresh alias string for every mix-view section on each redraw. Those fifteen paths are now created the first time the menu catalog is asked for them.
event.tempo_stretch accepts factor or length, and the Apply button was sending end_bpm, which the command ignored. A lower end tempo lengthens the selection, so the factor is the start BPM divided by the end BPM.
The bus row called mix.bus_rename and mix.bus_delete, which are not commands, so both buttons failed. setup.io already renames and deletes a bus by name.
A third built-in instrument next to Subtractive Synth and Drum Synth. It plays one
sample across the keyboard: root note, transpose and fine tune, key tracking, key and
velocity ranges, amp ADSR with velocity sensitivity, sample start, forward or ping-pong
loops, gate or one-shot playback, 32 voices with cubic interpolation. The sample's own
rate is converted as it plays.
- dsp: `Plugin::set_sample` and the `SampleSource` trait hand an instrument shared
audio without copying; the Adsr from the synth is reused.
- model: `Insert::sample` names the session source the sampler plays.
- mix: sync hands the pool audio to the instrument when it changes, and built-in
instruments now get their parameter values applied (they always ran on defaults).
- engine: `mix.sampler_load {track?, path, params?}` decodes WAV, AIFF, FLAC, MP3, OGG
(anything file.import_audio reads) into a session source, resampled to the session
rate and saved in Audio Files with the session; `mix.sampler_clear`; and
`mix.instrument_param {track?, param, value}` for any built-in instrument.
Tests generate their audio in code; no bundled assets.
Ctrl+Shift+Up/Down (#76) only worked on macOS. The registry's Ctrl there
is the Mac Control key, which has no key of its own on Windows and Linux
(#20), so the keys did nothing there, while the Shortcuts window and the
command palette still showed Ctrl+Shift+Up.
Pro Tools for Windows puts these on the Start key, but egui does not
report that key off macOS, and Windows and GNOME use Win/Super+Shift+
Up/Down to resize and move windows. So off macOS the nudge is
Ctrl+Shift+Up/Down: the same keys as on the Mac and what the labels
already say. No SoundCraft shortcut uses them there; Ctrl+Up/Down without
Shift still extends the selection.
Resolve System appearance from egui on every frame and fall back to Dark when the integration cannot report a theme. Keep receiving OS changes during manual modes and apply the selected palette to both widget style branches and the current root UI.
Remove the cached detector and its unused dependency. Cover startup, live transitions, manual overrides, saved System preferences, and user colors with an application-frame regression. Add reproducible offscreen system-theme inputs and document the fallback.
The input callback appended every block to one growing Vec: it
reallocated on the audio thread as the take grew, kept every input
channel in RAM, and the take existed only in memory until Stop, so a
crash during a take lost it.
- The input callback copies each block into a preallocated lock-free
ring (atomic slots, no unsafe) and never locks or allocates. Samples
that don't fit are paid back as silence before the next block, so the
take keeps its timing; the loss is reported in the status line.
- A writer thread drains the ring every 10 ms into one 32-bit float WAV
per armed track, holding only that track's input channels. Files go
to the session's Audio Files folder, or to its autosave folder while
the session has never been saved, named like the incumbent's takes
(<track>_<NN>.wav). An existing file is never overwritten.
- The header is rewritten every second, so a file left by a crash plays
up to the last update. Above 4 GiB it switches to RF64 in place.
- If a take file can't be created, Record doesn't start and says why;
a write error ends that take where it failed.
- At Stop the files become the takes' sources. Loop recording keeps one
file per track, one clip per pass (at most 1000) on its own playlist,
in one undo step.
- Saving uses a file already in the target's Audio Files folder in
place instead of rewriting it, so neither Save nor autosave copies a
take. A take recorded at another rate than the session's is saved once
at the session's rate.
soundcraft-audio-io gains wav_float_header (its length doesn't depend
on the frame count, so it can be rewritten in place); encode shares the
header code. Splitting a loop take into passes moved from the UI into
soundcraft_engine::io::add_recording, where it is tested.
Nothing in the Clip List could be removed: neither its files nor its clips
had a delete action, so everything stayed listed.
- New `clip.clear` (Cmd+Shift+B, the Clip List Clear shortcut in Pro Tools),
also offered as Clear… on every Clip List row's right-click menu. A clip
leaves its track; a file leaves the session with its decoded audio and is
never deleted from disk. One undo step brings everything back.
- As in Pro Tools, Clear asks first. The dialog names what goes, including
the clips on any track or playlist that still play a file being removed:
they are removed too. Programmatic calls never open it; they pass
`with_clips: true` to remove a file still in use, and fail without
changing anything otherwise.
- File rows can be selected now (`edit.select {sources}`, kept in
`EditState::selected_sources`); with no ids, `clip.clear` acts on the
Clip List selection.
- Remove Warp and re-warping ignore a stored original whose file has been
cleared, so they never point a clip at a missing file.
* Add a selectable Light theme
* Apply the active theme to the menu bar
* Use the system theme by default
* Expose Appearance in the Setup menu
* Add Light theme screenshot
* Remove the Light theme screenshot
Repo policy keeps large binary assets out of the tree, and the PNG had no
ATTRIBUTION row.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Appearance: ui.theme command, Dark default, non-blocking system lookup
- ThemeMode derives Default with Dark as the default (fixes clippy's
derivable_impls); System and Light are opt-in. LIGHT_THEME starts false.
- New UI command `ui.theme {"mode": "system|light|dark"}` (label
"Appearance") in UI_COMMANDS; with no mode it reports the current one.
The UI Customization combo box dispatches it instead of writing state.
- The choice lives in UiState.theme, so it persists with the existing UI
prefs (ui.json) and is exposed through ui.set / ui.inspect. Restoring a
window configuration keeps the current theme.
- dark_light::detect() is only called in System mode, on a background
thread (an AtomicU8 holds the answer; dark until it arrives, then a
repaint). On the web it is the synchronous matchMedia lookup.
- theme::apply runs only when the effective palette changes, not every
frame.
- window.ui_customization keeps its catalog path (Window > UI
Customization) so parity is unchanged; Setup > Appearance still opens
it. The combo row is labelled "Theme" so it no longer repeats the title.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Light theme: move hard-coded colours to theme tokens
The Light palette left dark islands and unreadable text where colours
were Color32 literals. New tokens (Dark values are the old literals, so
the Dark theme renders pixel-identical to main):
- menu bar; toolbar group outline; edit-mode cells (no more dark block
around Shuffle/Spot/Slip/Grid); counter panel outlines, dividers and
off toggles; Smart-tool bracket
- selected rows in the Tracks/Groups/Clips lists, track-visible dot,
selected Edit track header, selected Mix strip
- marker names, alternate-playlist lanes, Universe background/outline,
Big Counter background
- meter background and clip lamp, pan knob body/ring/pointer, Mix insert
and send slots, Mix section captions
In Light, clip bodies are a paler tint and waveforms a deep shade of the
clip colour instead of black.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Light theme: instrument slot and plugin window text use the current tokens
The instrument picker (#23) landed with Tokens::DARK literals.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: t <t@t>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>