- A muda error now logs a warning and keeps the in-window menu bar
instead of aborting startup (never crash).
- sync() walked ~500 leaves with a command lookup and a Cocoa call each
frame; run it at most 4x/s and right after a click.
- About / Session Info from the application menu open their window
rather than toggling it closed.
- The title carries Edit/Mix and the dirty mark only when the system
menu bar replaces the egui one (no duplicate on Linux/Windows).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
egui panics on Type 1 or bitmap font bytes, which fc-match can return on
minimal systems, so check the sfnt magic first. Mask the variable-font
named instance out of %{index}. Fix the unused 'bold' warning on macOS
and Windows.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The stopped position does not advance, so a beat at the stop point
re-rendered its first 30 ms every block: a ~94 Hz buzz whenever a track
was record-armed or monitoring input. Regression test included.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stop no longer writes the session's scroll directly (everything is a
command) and leaves the view alone when the stopped position is visible.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On macOS a held Cmd made the Ctrl state ignored, so Cmd+Ctrl+X ran Cut
instead of Cut Clip Gain. Ask egui for the OS so the web build in a Mac
browser uses Mac modifiers too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
VST3 editor windows are hosted on macOS only, but editor_main_thread and
fixture_e2e asserted a probed view size on every platform, failing the
FreeBSD job on every commit to main (and Linux locally). Gate only the
size assertion; the controller round trips still run everywhere.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cpal (wasm) creates its output stream at startup, before any user gesture, so the browser leaves the WebAudio AudioContext 'suspended'. As a result no audio plays, and the transport clock never advances because the audio callback drives it.
Wrap the AudioContext constructor before the wasm module loads so that every context cpal creates is captured. On the first pointer, mouse, touch or key event, resume any context that is still suspended.
Signed-off-by: Ahmed Moussa <moussa.ahmed95@gmail.com>
The DMG opened as a plain Finder window. It now shows the app icon's figure on
the SoundCraft colour, with SoundCraft.app and the Applications link side by side.
Same approach as PhotoCraft's and VectorCraft's (storytold/photocraft#1201,
storytold/vectorcraft#493).
- packaging/macos/dmg/: background.svg (source; text outlined, no paper grain,
the figure an <image> of assets/app-icon), background.tiff (1x + 2x HiDPI,
16-colour palette, Deflate, sRGB; ~28 KB)
and dmg-layout.DS_Store (Finder view settings: window, icon size 128,
positions, background).
- generate.py writes the TIFF and the layout on any OS, byte-for-byte
reproducible, with nothing from the machine that ran it (no paths, users,
disk names or volume UUIDs).
- package.sh copies them into the staging folder; the image is still built with
hdiutil makehybrid. Nothing extra runs in the signing job.
- The volume name drops the version: the layout finds the background through
an alias that includes the volume name. The DMG file name keeps the version.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
eframe's default wgpu instance enables PRIMARY | GL on Windows. Creating
the OpenGL backend can crash inside AMD's atio6axx.dll before the window
appears, so the app exits silently at launch. When WGPU_BACKEND is not
set, the Windows instance now enables only DirectX 12; an explicit
WGPU_BACKEND is still honoured. macOS, Linux and web are unchanged.
Same fix as storytold/designcraft#168 and storytold/filmcraft#179.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyVbT6Vv4BwWZ4hY8973qD
The clip menu was attached at the end of lane_interaction, which returns
early when the lane has no pointer position. Once the menu opened, the
pointer sat over the popup, the lane lost its hover, and the menu was
never drawn again, so it vanished before Delete could be clicked.
Attach the menu before the early return and remember the right-clicked
clip so it keeps its target. Stale clip ids are dropped if the clip no
longer exists. Adds a kittest regression test covering right-click,
move onto the menu, and Delete.
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
The monitor ring fed raw input-device samples to the output callback as if
they were at the session rate. A 16 kHz or 44.1 kHz input played back too
fast (higher pitch) and ran dry between bursts, so it sounded quiet.
InputRing now knows the input rate and converts to the session rate with
linear interpolation in pop_into, allocation-free and non-blocking. The
buffer cap is computed once in new(). Tests cover rate up-conversion,
down-conversion, equal rates and underrun.
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
The tempo-change dialog clamped BPM to 1..=999, but the engine accepts
5..=1000. A 1000 BPM tempo was silently changed to 999 on OK, and values
below 5 were rejected after the dialog had closed.
Use the engine's limits, and move the OK action into tempo_change_action
so a test can check it runs a real command with the position. The test
would have caught the original bad command id.
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Options > Click set a flag but nothing rendered sound, so a click track
was silent. The mix engine now synthesizes a short blip on every beat of
the tempo map (accent on beat 1), using the click.* settings from
Setup > Click/Countoff, and sums it after the master fader.
The metronome is opt-in per engine: only live playback enables it.
Offline renders (bounces, stems, exports) leave it off so the click is
never printed into a file.
Also: midi_to_hz moves from the engine demo into soundcraft-dsp so the
mix and demo share one tuning formula.
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Double-clicking the tempo ruler opened a number dialog whose command was
built as "event.tempo@<position>". No such command exists, so OK failed
and nothing changed. The position was also never passed, so a change
would have landed at the session start.
Add a TempoChange dialog that carries the position and runs
event.tempo with bpm and at. Add an engine test that a tempo change at a
position is applied there.
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
There was no way in the app to choose a CLAP, VST3 or Audio Units instrument,
or to open an instrument's editor:
- the insert menus leave instruments out;
- an instrument was only set by track.new;
- the Mix window's INSTRUMENT row looked for instrument inserts rather than the
track's instrument;
- plugin windows opened only for insert slots.
Changes:
- mix.instrument {track?, plugin} sets an instrument track's instrument, with
the plugin's default parameters, and is undoable. It refuses effects and
other track kinds.
- The INST label on an instrument track's header opens an instrument menu: the
current instrument and its editor, then the built-in, CLAP, VST3 and Audio
Units instruments, by vendor.
- The Mix window's INSTRUMENT row shows the track's instrument. A click opens a
hosted instrument's plugin window, and a right-click changes it.
- Plugin windows open for the instrument slot, with only the editor button,
because the bypass, preset and parameter commands address insert slots.
They no longer derive a slot letter for the instrument slot.
Closes#22
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Mix window's insert menu listed each format's plugins as one flat column.
With many installed (53 VST3 and 95 Audio Units effects on a composer's Mac)
it ran past the bottom of the screen and egui menus don't scroll, so the lower
entries couldn't be reached. CLAP, VST3 and Audio Units now open one submenu
per vendor, as Pro Tools does when plug-in menus are organized by manufacturer,
and every list scrolls within the screen.
Choosing an Audio Unit did nothing: the engine's plugin lookup, which
mix.insert uses, covered built-in, CLAP and VST3 plugins only, so "au:" ids were
"unknown plugin" and the UI dropped the error. The lookup now includes Audio
Units (the engine depends on soundcraft-au-host, like the mixer and the UI), and
the menu shows a failed insert in the status bar.
Closes#21
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The system Quit (⌘Q, or a quit Apple Event) goes through
-[NSApplication terminate:], which calls exit() from inside the event loop, so
eframe::run_native never returns and the shutdown placed after it never ran.
With HALion Sonic in use, the exit handler then ran its bundleExit too late and
the app crashed on quit ("quit unexpectedly", found in manual testing).
eframe calls on_exit on every way out, so the app now releases its player (and
with it the plugin instances) and shuts plugin hosting down there.
Refs #16
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Testing on macOS showed that the exit-handler fallback comes too late for a
plugin that has been used: with HALion Sonic inserted on a track, its bundleExit
segfaults, and both the app (on quit) and soundcraft-cli (after a bounce) exited
with 139. A plain scan was fine. Exit handlers run in reverse order of
registration, so state the plugin set up while in use is presumably torn down
before the host's handler runs.
soundcraft_vst3_host::shutdown() runs the module exits up front, and
soundcraft_engine::shutdown_plugin_hosts() calls it. The app calls that once the
event loop has returned (the player and its plugin instances are gone by then),
and the CLI once its subcommand has. The exit handler stays as the fallback for
other exits, such as test binaries.
The fixture can now set up state lazily the way such plugins do
(VST3_FIXTURE_LATE_STATE), and module_exit_shutdown checks both sides in child
processes: with shutdown() the exit is clean, with only the exit handler the
fixture reports the late teardown. Contract violations now end the process with
status 3 instead of an abort, so that expected failure leaves no crash report.
Refs #16
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
egui reports a held Ctrl as both ctrl and command off macOS, so no Cmd+ shortcut matched and Ctrl+S opened Search (Cmd+Ctrl+S) instead of saving. Off macOS a held Ctrl now stands for the shortcut's Cmd, shortcuts that also need the Mac Control key stay off the keyboard there (Ctrl+X is Cut, not Cmd+Ctrl+X), and the Search chord is macOS only.
vst3-host ran each module's entry (bundleEntry / InitDll / ModuleEntry) when it
loaded a .vst3 and cached the module for the life of the process, but never ran
the matching exit or released the cached factory. At exit() the module's static
destructors then ran on a module that was never shut down. Steinberg's HALion
Sonic segfaults there, so every process that had scanned VST3 plugins crashed on
exit: the app on quit, soundcraft-cli, and the engine, mix and vst3-host unit
tests.
At process exit each module now has its factory released and its exit run,
newest first, on the thread that ran its entry: Native Instruments' Massive X
crashes in bundleExit on any other thread. Loads asked for off the main thread
go to a long-lived module thread, which also runs their exits. Modules with a
live instance or a leaked editor view are left alone, later loads are refused,
and plugin code stays mapped. The exit handler is registered with atexit after
each new module, so it runs ahead of that module's static destructors; it uses
OS thread ids because std::thread::current() panics in exit handlers. A module
whose entry ran but that then fails to load now gets its exit too.
The fixture plugin now checks the module lifecycle and aborts the process when
it is broken. Three harness = false tests cover the exit after use, a live
instance at exit, and a load from a short-lived thread.
Fixes#16
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- AppImages embed gh-releases-zsync update information and ship a .zsync beside them.
- New flatpak job repackages the Linux tarball into soundcraft-<v>-linux-<arch>.flatpak
(bundle manifest + flatpak-bundle.sh). Sandbox: pulseaudio socket (PipeWire's pulse server),
~/.vst3 and ~/.clap read-only, the LinuxAudio.Plugins extension point via VST3_PATH/CLAP_PATH;
no raw ALSA devices, no JACK. The from-source Flathub manifest gets the same finish-args.
- Only signing jobs (macOS, Windows) and the draft job keep `environment: release`, so a branch
dispatch dry-runs Linux, Flatpak, FreeBSD and web; the draft job still needs the signed jobs.
- README: Downloads section; docs/releasing.md updated.
Co-authored-by: Brandon Thomas <shared2@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The vst3 bindings' enum constants use the C default enum type, which is i32 on
Windows and u32 elsewhere. The bus helpers took u32 and the process-context
flags were assigned to a u32 field directly, so the release build failed on all
three Windows targets. Type the helpers with the bindings' own aliases and cast
the flags once.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The desktop app installed no logger, so every log::warn!/log::error! in
the workspace was discarded: the engine's panic report, audio devices
that failed to open or broke, plugins that refused their stored state or
failed to activate, CLAP plugins' own log messages, files that failed to
open. A launch from a desktop menu has no terminal either, so there was
nothing to attach to a bug report.
apps/soundcraft/src/logging.rs (no new dependency):
- standard error plus <settings dir>/logs/soundcraft.log (beside
ui.json, Autosave/ and Presets/); each launch rotates the previous log
to soundcraft.1.log and .2.log, so the log of a run that crashed
survives the next start; the file is capped at 16 MiB
- records logged before the settings directory is known are buffered
(bounded) and written once the file is attached
- default levels: info for soundcraft*, warn for everything else;
RUST_LOG replaces them with env_logger-style directives
- never panics: an unusable directory or a failed write leaves standard
error as the only sink; a poisoned lock is tolerated
- runs with SOUNDCRAFT_NO_PREFS (agents' test runs) log to standard
error only, so they don't rotate away the user's own logs
Realtime audio: a record takes a lock and does I/O, which the audio
callback must never do, yet code on the audio thread logs (the cpal
error callbacks, the synth's full event queue, a hosted plugin's failed
process, a CLAP plugin calling the host's log, the panic hook). The cpal
callbacks now mark their thread (soundcraft_mix::mark_audio_thread,
re-exported by soundcraft_playback), and the mix engine carries the mark
into the rayon workers that render strips for it. A record from a marked
thread is not written there: the first is kept in a fixed buffer
(try_lock, no allocation, never waits), the rest are counted, and the UI
thread writes them every frame (any other thread before its own next
record).
The logger is installed first in main (before the engine's panic hook)
and the file is opened after argument parsing (--version leaves no
file). The settings directory moves out of prefs_path into config_dir.
The control server's eprintln!s go through the logger (the
SOUNDCRAFT_CONTROL_PORT line on standard output stays). Documented in
README.md (Getting started > Logs) and docs/architecture.md (Audio).
Custom macOS larger runners in a runner group do not pick up jobs; the
built-in M2 Pro label does. Only release.yml uses it, and the
actions_macos_xl budget caps the monthly spend.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The standard macOS runners are shared with all CI and queue for an hour or
more. The macos-release runner (GitHub-hosted M2 Pro) is in a runner group
usable only by release.yml on the release branch, with a monthly spend cap
on the actions_macos_xl SKU.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* About: contributors compiled in from contributors.json (grab bag + table, sortable, name toggle)
Adds the craftrules standard (standards/contributors.md) to SoundCraft: the About window gets
About · Contributors · Models tabs. contributors/contributors.json is generated by
craftrules/scripts/contributors.py and baked into the binary by crates/ui-egui/build.rs as
static tables. Each contributor line: @username, opt-in display/real name, PRs, commits,
lines +/−/Δ, binary assets +/−, first/last commit. Sortable by every column; alphabetical
ignores case and the leading @. Sort arrows use ⏶/⏷ (egui's icon font) since the system UI
font has no ▲/▼.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Contributors: self-submitted names in contributors/people.toml; AGENTS.md credit-yourself section
build.rs reads contributors/people.toml next to the generated contributors.json and shows a name only
for entries the generator verified as written by that person (craftrules standards/contributors.md).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Contributors: regenerate (verified people.toml entries)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Video (new standalone crate soundcraft-video):
- Pure-Rust H.264 (Baseline/Main/High), ProRes (all flavours) and Motion JPEG decoding with
ISO-BMFF demux, adapted from FilmCraft's crates; fuzzed with mutated/truncated movies
- TrackKind::Video with ClipContent::Video referencing Session.videos; file.import_video adds
the picture track and the movie's audio; relinking on open; video.offset/online/list
- Video and Video Universe windows, thumbnails on the timeline, background decoding
Third-party plugins:
- Plugin state (CLAP state ext, VST3 component + controller state) saved in sessions
- Hosted instances created, state-loaded and dropped on the UI thread, never the audio thread
- Plugin editors: floating CLAP GUIs and VST3 views on macOS (verified with Melodyne)
- New isolated FFI crate soundcraft-au-host: Audio Units on macOS (effects and instruments,
parameters, latency, MIDI, ClassInfo state), wired into the mixer and plugin browser
UI: Window > Renderer (speaker layout with live levels and object positions), Window > UI
Customization (interface scale, reduce motion), Window > Configurations > Auto-Update.
Fix: saturating sample-index math in clip rendering (hostile stretch values overflowed).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- File › Export › Sibelius and File › Send To › Sibelius write MusicXML 4.0 (quantized,
chords, ties across bar lines, meter changes, treble or bass clef per part)
- File › Print Score engraves the MIDI tracks to a printable SVG page; File › Score Setup
sets title, composer, bars per system and track names
- File › Import › Clip Groups and Export Clip Groups (.scgrp with its audio)
- Edit › Paste Special › Merge Markers: copy carries the markers in the copied range
- The UI opens a path prompt for any command that needs a path or folder
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Surround:
- main and bus formats up to 9.1.6 and Ambisonics; pairwise constant-power
surround panner (divergence, centre, LFE, height) per track and send;
ITU fold-down; multichannel bounce with correct WAV channel masks;
multichannel device output; surround panner and multichannel meters in the
Mix window; setup.main_format, mix.surround_pan, mix.send_surround_pan
VST3:
- soundcraft-vst3-host: isolated unsafe crate (vst3 bindings, MIT), scans
standard folders, hosts components/processors/controllers, params,
latency, notes; VST3 menu; engine.vst3_plugins; e2e tests with a fixture
Hardening:
- positions from parameters clamped; offline renders capped and bounded to
track material; loop/transport limits; hostile-parameter property test
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>