No description
  • Rust 98.4%
  • Shell 0.8%
  • PowerShell 0.7%
  • Python 0.1%
Find a file
aiongg e82c1689b3
Some checks are pending
ast-grep / scan (push) Waiting to run
CI / gate (push) Waiting to run
Nix / nix (push) Waiting to run
Nix / Git dependencies come from storytold (push) Waiting to run
SonarCloud / sonarcloud (push) Waiting to run
Packaging lint / lint (push) Waiting to run
Packaging lint / Windows installation policy (no app build) (push) Waiting to run
refactor(cli): parse soundcraft-cli arguments with clap, following RFC-0012 (#1136)
* test(cli): pin today's argument handling

Run the real binary and record how the hand-written parser behaves for every
subcommand: help and version spellings, no arguments, unknown subcommands and
flags, value flags in both spellings, missing and repeated values, positional
order, exit codes and which stream carries each message.

No behaviour change. Later parser changes update these assertions on purpose.

* refactor(cli): split main.rs into modules

Move only: args, error, output, convert, info, session, catalog, app and
mcp each get their functions and unit tests; main.rs keeps the usage doc,
help_text and the dispatch. Moved items become pub(crate) where other
modules call them. No behaviour change.

* refactor(cli): typed errors and run(args, out, err)

soundcraft-cli is now a library plus a thin binary. `soundcraft_cli::run(args,
out, err) -> i32` is the whole program; main.rs only passes the real streams
and shuts the plugin hosts down afterwards (process teardown, not part of run,
which tests call repeatedly in one process). Subcommands write to the given
writers, return `Result<(), CliError>`, and `fail` is gone. CliError wraps
engine, audio-io, io (with the path), serde_json and the control channel
errors; UsageError, ConvertError and RootFlagError cover the subcommands' own
failures. No Result<_, String> is left except where a dependency reports a
string (control-channel call, AutomationRoots::new), wrapped in a variant.

The tests that need no files, working directory, environment or stdin now call
`run` in-process (tests/it/run.rs); the rest still run the binary.

Behaviour changes:
- Error messages start with `error: ` instead of `soundcraft-cli: `.
- A script line that stops the run now prints `error: FILE:N: ID: ...` (it had
  no prefix). With --keep-going the line stays bare.
- A convert encode or write failure now names the output path.
- An empty or help-less call still prints the one-line usage to stderr without
  a prefix and exits 1; `files confined:` keeps its `soundcraft-cli:` prefix
  (it is a note, not an error).

* refactor(cli): parse arguments with clap

Replace the hand parser (arg_value, check_options, ENGINE_OPTIONS, the
lib.rs dispatch, the per-subcommand positional handling and the root
flags' consume) with clap derive types in args.rs: a Parser, a
Subcommand enum and one Args struct per subcommand, with the run/script
engine flags flattened. main passes args_os(); run takes OsString-like
arguments and renders clap's help, version and usage errors (help and
version on stdout, exit 0; usage errors on stderr, exit 2). Add
command() returning the clap definition. The usage block in the module
doc and help_text are gone: the doc comments are the help.

Behaviour changes:
- Usage errors exit 2 with clap's message and the --help hint on
  stderr, instead of exit 1 with hand-written text: unknown subcommand,
  unknown flag, missing value, missing or extra positional, a value on
  a boolean flag (--demo=1, --normalize=1).
- No arguments prints the help to stderr, exit 2 (was a one-line usage,
  exit 1).
- --help, -h and help print clap's generated help, and every subcommand
  has its own (SUB --help, SUB -h, help SUB). Before, info and script
  read --help as a file name, describe as an id, commands as a filter,
  run rejected --help and ignored -h, convert and app printed their
  usage error, mcp, parity and plugins ignored it, and app --port P -h
  called the method -h.
- --help or -h before -- shows the subcommand's help even after a
  mistake on the same line (run --bounce --help).
- --version, -V and version print "soundcraft-cli X.Y.Z" (was
  "SoundCraft CLI X.Y.Z").
- Unknown flags on info, commands, describe, app, mcp, parity and
  plugins are usage errors; they were ignored or read as a positional.
  mcp --automation-bogus exits 2 (was exit 1).
- Extra positionals are usage errors on every subcommand (were ignored).
- A single-dash word (-x) is a flag, so an unknown one is a usage
  error; a value starting with - is written --flag=-value.
- Flags may come before or between the positionals: convert IN --rate
  44100 OUT, convert --rate 44100 IN OUT and script --keep-going FILE
  parse (the flag word was taken as a file name).
- A repeated single-value flag keeps its last value (was the first):
  --rate, --bit-depth, --save, --port and the others.
- parity --write without a value is a usage error (printed the report).
- mcp --connect without a value, or followed by a flag, is a usage
  error (was exit 1, or took the flag as the port).
- mcp --demo=1 is a usage error (was ignored, without the demo).
- A root flag without a folder, with an empty one, or followed by a
  flag is a usage error with clap's message (was exit 1).
- A non-Unicode argument is a usage error where text is expected and
  accepted as a root folder (std::env::args panicked).
- The help lists --sample as --demo's alias.

Unchanged: --dither with --no-dither, bad --rate and --bit-depth values,
the roots with --connect and the SOUNDCRAFT_CONTROL_PORT fallback are
still checked after parsing (exit 1).

* refactor(cli): reject repeated flags, clap --version (photocraft#7071 decisions)

Behaviour changes:
- A flag given twice is a usage error (exit 2), in either spelling, including
  an alias pair such as --demo and --sample. Before, the last one won. Only
  run's --cmd and mcp's --automation-read-root / --automation-write-root
  repeat; they accumulate in order.
- --version and -V print the long version, "soundcraft-cli 0.13.1 (linux
  x86_64)": the version and the platform the binary was built for.
- The hidden `version` subcommand is gone; `soundcraft-cli version` is an
  unrecognized-subcommand usage error.
- --start and --end accept a negative number (`--start -1`, as the engine
  counts positions before 0); any other value starting with `-` needs `=`
  and otherwise is an unexpected-argument usage error. No flag has
  allow_hyphen_values.
- `help <subcommand>` is unchanged (tested).

* refactor(cli): typed values and flag relations

Handlers receive typed values: convert --bit-depth is a ValueEnum, --rate a
NonZeroU32, file arguments PathBuf, --start/--end a TimeArg, --cmd a parsed
(id, JSON) spec, app's JSON a serde_json::Value, app --port and mcp --connect
a validated port or host:port. Relations (--dither/--no-dither, --in/--demo,
--start/--end needing --bounce) are declared on the flags. describe ID,
commands FILTER and app METHOD refuse an empty string. parse_rate,
parse_bit_depth, parse_dither, parse_cmd_spec, time_arg and the Rate,
BitDepth, DitherConflict and CmdJson errors are gone.

Behaviour changes:
- --dither with --no-dither is a usage error (exit 2) instead of exit 1.
- --rate 0 / non-numeric and an unknown --bit-depth are usage errors (exit 2)
  with clap's wording, naming the flag. --bit-depth help lists the values.
- --in with --demo (or --sample) is a usage error; --in used to win silently.
- --start/--end without --bounce are a usage error; they used to be ignored.
- Malformed JSON in --cmd ID=JSON and in app's JSON argument is a usage error
  (exit 2) at parse time. Before, run opened the engine and executed every
  earlier --cmd (printing their results) before the bad one failed with exit 1.
- An empty --start/--end, an empty command id (--cmd =1), and an empty
  describe ID, commands FILTER or app METHOD are usage errors. An empty
  commands FILTER used to list everything.
- app --port and mcp --connect accept only a port (0-65535) or host:port;
  junk used to be accepted and failed at connect time (exit 1).
- Non-Unicode file names are accepted wherever the callee takes a Path (info
  audio files, convert, script, parity --write). Session and bounce/save paths
  go to the engine as text, so a non-Unicode one fails with exit 1, "the path
  is not valid Unicode", after any earlier --cmd ran.

* refactor(cli): shared launch flags and SOUNDCRAFT_CONTROL_PORT as clap env

soundcraft_automation::args holds the launch flags as clap Args structs:
ControlClientArgs (`app --port`, env SOUNDCRAFT_CONTROL_PORT, default
7979), ControlServerArgs (the desktop `--control`, same variable, wired
in a later step; its ControlPort value is a port, also from a loopback
host:port, or another host's host:port, which the app decides on, so one
export serves both), AutomationArgs (the two root flags, repeatable, flags
only), the ControlTarget value parser, and the blank_env_is_unset,
name_env_vars and conflict helpers. The module and its clap/thiserror
dependencies are left out of the wasm build. `app` and `mcp` flatten
them; the hand fallback to the variable in `app` is gone.

Behaviour changes:
- An empty or blank SOUNDCRAFT_CONTROL_PORT counts as unset, so `app`
  uses 7979; before, it tried to connect to `127.0.0.1:` (exit 1).
- A bad SOUNDCRAFT_CONTROL_PORT is a usage error naming the variable
  (exit 2); before, the connection failed (exit 1).
- Spaces around the `app --port` or `mcp --connect` value, or the
  variable's, are ignored; before, the connection failed (exit 1).
- An IPv6 host in `app --port`, `mcp --connect` or the variable goes in
  brackets (`[::1]:7979`); `::1:7979` is a usage error (exit 2), as the
  host and port can't be told apart; before, the connection failed
  (exit 1).
- `app --help` shows `[env: SOUNDCRAFT_CONTROL_PORT=]` and
  `[default: 7979]` instead of the hand-written default note.
- `mcp` with a root flag and --connect is clap's conflict error (exit 2,
  "the argument '...' cannot be used with '...'"), reported before any
  root folder is opened; before, a custom message with exit 1, after the
  roots were opened.

* refactor(app): move launch flag parsing into launch_args

The parsing moves out of main unchanged, into a LaunchArgs struct and a
parse function that takes SOUNDCRAFT_CONTROL_PORT as a parameter. Tests pin
the flags, the file list, ignored unknown flags and the fallback of a bad
--control to the variable. No behaviour change.

* refactor(app): parse launch flags with clap

soundcraft --help now comes from the LaunchArgs doc comments and ends with the
environment variables the app reads, and the shared ControlServerArgs supplies
--control and SOUNDCRAFT_CONTROL_PORT. Process tests cover the variable (bad
value, blank value, loopback host:port, another host).

Behaviour changes:
- An unknown flag (--bogus, -x) is a usage error on stderr, exit 2. It was
  ignored, and a single-dash word was opened as a file; after `--` it is a file.
- A bad --control (out of range, not a number, missing value) is a usage error,
  exit 2. It was ignored and the variable used instead.
- A bad SOUNDCRAFT_CONTROL_PORT is a usage error naming the variable, exit 2.
  It was ignored. An empty or blank variable still counts as unset.
- A loopback host:port (127.0.0.1, localhost or [::1]) in --control or
  SOUNDCRAFT_CONTROL_PORT gives its port, so the value `soundcraft-cli app`
  takes from the variable works for both. The control server listens on
  127.0.0.1 only, so another host in --control is a usage error. In the
  variable, which may point `soundcraft-cli app` at another machine, it is
  ignored with a log warning and the app opens without a control server.
  Both were ignored before.
- A repeated flag is a usage error (--demo twice, --control twice).
- --control=PORT and a port with surrounding spaces are accepted; a value that
  starts with `-` is not taken as the value, except a negative number, which is
  a bad port (`--control -1`).
- --help ends with an Environment section listing SOUNDCRAFT_CONTROL_PORT,
  SOUNDCRAFT_CONFIG_DIR, SOUNDCRAFT_NO_PREFS, SOUNDCRAFT_NO_SYSTEM_FONTS,
  SOUNDCRAFT_WAYLAND (Linux) and RUST_LOG.
- --help and -h print the help, exit 0. They were ignored.
- --version and -V print `soundcraft 0.13.1 (linux x86_64)`, as soundcraft-cli
  does. It printed `SoundCraft 0.13.1`.
- -V and --version are read in order with the other arguments: an earlier
  error comes first.
- macOS -psn_… process serial numbers are dropped, as before they were
  opened as files.
- A file whose name is not valid Unicode is skipped with a log warning. It
  used to panic in std::env::args.

* feat(cli): shell completions and man pages

An example, `cargo run -p soundcraft-cli --example man-and-completions -- <out-dir>`,
writes bash, zsh, fish, elvish and PowerShell completions and soundcraft-cli.1 plus a
page per subcommand, using clap_complete and clap_mangen (dev-dependencies of
soundcraft-cli only, not in the binary).

The Linux package (deb, rpm, tar.gz, AppImage) installs the pages in
/usr/share/man/man1 and the bash, zsh and fish completions in their standard
directories; the deb also links the zsh completion into Debian's
vendor-completions. A cross build runs the example through $EMULATOR and
leaves the files out without one. The FreeBSD tarball carries them in the
ports layout; a cross build builds the example for the host as well.

No change to the CLI's behaviour. A test pins the files the example writes.

* ci(cli): check the command-line code with ast-grep rules

Adds lint/ast-grep (rules for clap errors built from strings, String as an
error type, and std::env::args) and a workflow that runs the rule tests and a
scan over the CLI code through the shared craft-common-actions workflow. No
behaviour change; the scan has no hits.

* docs(cli): document soundcraft-cli and RFC-0012

Add docs/cli.md: the subcommands, exit codes, environment variables, the
desktop app's launch flags, man pages and completions, and the RFC-0012 rules
not followed yet (convert can overwrite its input; no --compact/--json).
Link it from the README and the MCP and control-protocol guides. No code change.

* feat(cli): refuse an output that replaces an input

An output that is one of the subcommand's inputs is a usage error (exit 2,
`error: …` on stderr, clap's usage line), reported while parsing, before
the engine loads or any --cmd runs. Paths are compared by file identity
(device and inode, through same-file), or, when neither exists yet, by
where they would be created, so `a.wav`, `./a.wav`, an absolute path and a
symbolic or hard link all count as the same file.

Inputs and outputs per subcommand:
- convert: input IN, output OUT. --in-place lets OUT replace IN.
- run: input --in, outputs --bounce and --save. --in-place lets --save
  replace the --in session; it requires --in. A --bounce naming the --in
  session is always refused.
- script: inputs FILE and --in, outputs --bounce and --save. --in-place
  lets --save replace the --in session; a --bounce naming it is always
  refused, and neither may ever replace FILE.
- run and script: --bounce and --save naming one file is refused with or
  without --in-place (the save would replace the bounce).
- info, commands, describe, app, mcp, plugins write no file; parity
  --write has no input. Paths inside --cmd parameters are not checked.

With --in-place, an output that is a symbolic link to the input is written
to the file the link names, so convert and --save both replace the input
and leave the link in place (a save renames a new file over its path,
which would otherwise replace the link and leave the input unchanged).

Behaviour changes:
- `convert a.wav a.wav`, `run --in s.scraft --save s.scraft`,
  `run --in s.scraft --bounce s.scraft` and the other cases above exit 2
  instead of replacing the input.
- New flag --in-place on convert, run and script.
- An existing file that is not an input is still replaced without asking.

* feat(cli): --compact for JSON output

RFC-0012 rule 1.3: JSON is pretty-printed by default and --compact prints
one line. Every pretty-printed JSON output now goes through
output::print_json.

Changed outputs:
- info <audio file>, describe, app: new --compact flag.
- run and script: new --compact flag, which requires --inspect and applies
  to the --inspect JSON only.

Unchanged on purpose:
- The `ID: JSON` result lines of run and script and the --bounce and --save
  results stay one line each. The prefix makes them text records, and the
  RFC lets a command document its own layout; scripts that parse them keep
  working.
- commands, plugins, parity and the .scraft summary of info are text and get
  no --json flag (an open question in the RFC); mcp speaks JSON-RPC lines.

Pretty output is byte-identical to before.

* fix(app): one Switch parser for boolean environment switches

SOUNDCRAFT_NO_PREFS, SOUNDCRAFT_NO_SYSTEM_FONTS and SOUNDCRAFT_WAYLAND each
counted any value, including 0, as on. They now share
soundcraft_ui_egui::switch::Switch: 1/on/true/yes is On, 0/off/false/no is
Off (case- and space-insensitive), unset or blank is Default, and any other
value is Default and logged with the variable name. The ENVIRONMENT help
text, docs/cli.md and the README say the same.

Behaviour changes (old -> new):
- SOUNDCRAFT_NO_PREFS=0 disabled preferences (any value did); now only On
  does.
- SOUNDCRAFT_NO_SYSTEM_FONTS=0 disabled system fonts (any value did); now
  only On does.
- SOUNDCRAFT_WAYLAND=0 kept native Wayland (any value did); now only On does.
- An empty value of any of the three counted as set; it now means the
  default (off).
- An unknown value of any of the three is logged.

* refactor(app): read the environment once into Env

The desktop app reads SOUNDCRAFT_CONFIG_DIR, SOUNDCRAFT_NO_PREFS,
SOUNDCRAFT_NO_SYSTEM_FONTS and SOUNDCRAFT_WAYLAND once at startup into
env::Env (typed fields, Switch for the on/off ones), after the logger is
installed so malformed switches are logged. The --help Environment
section is generated from the same VARS table, with a test that it lists
every name.

ui-egui no longer reads the app's variables: fonts::install and
definitions take system_fonts, set through SoundApp::set_system_fonts
(default on, so the web app is unchanged). The XWayland and prefs
decisions are methods on Env, tested on literals. logging::install takes
the RUST_LOG value, which stays outside Env because the logger needs it
before anything can log.

No behaviour change beyond help column spacing.

* refactor(cli): one long_version for both binaries

* refactor(audio-io): one FileFormat table for extensions

FileFormat gains the extension table every caller matches on:
from_extension / from_path (ASCII case-insensitive) over extensions,
from_write_extension over write_extensions, default_extension, name,
mime_type, the READ and WRITE lists, and readable_extensions /
writable_extensions for dialog filters, help and messages. Reading
takes every extension the decoder knows; writing takes only the names
whose readers expect what the encoder writes (no rf64 or aifc, whose
files would get plain RIFF and AIFF headers).
detect_format, bounce (mix and stems), convert, the bounce dialog, the
desktop and web file pickers, the workspace browser, the Sampler, web
downloads and the MCP bounce schema all use it; ENCODE_EXTENSIONS and
encode_format_for are gone. convert's IN/OUT and info's FILE help name
the extensions from the read and write lists.

Behaviour changes, where the lists disagreed:
- The bounce dialog's Browse result, the bounce save filter and the MCP
  bounce enum take wav, wave, bwf, aif, aiff and flac: wave and bwf are
  new to all three, and aif is new to the MCP enum. convert and
  file.bounce_mix already took these. The "supported:" list in the
  cannot-write errors is "wav, wave, bwf, aif, aiff, flac".
- file.bounce_stems reads its format name case-insensitively: `AIFF`
  or `FLAC` meant WAV before. Unknown names still mean WAV.
- The bounce dialog sends the format as its file extension (aif for
  AIFF); the engine accepts both spellings.
- File pickers, the workspace browser and the Sampler's drop and Load
  Sample filter list every extension the decoder reads: the workspace
  browser gains wave, bwf, rf64, aifc, mp1, mp2, oga, aac, mp4, adts,
  mkv and webm; the Sampler gains rf64, mp1, mp2, m4b, adts, mkv and
  webm; the desktop and web open filters gain rf64, mp1, mp2 and adts.
- Web downloads get a media type for every audio format, not only WAV,
  AIFF and FLAC.

* refactor(app): resolve the settings folder once in Env

* fix(app): keep paths as paths until the engine needs text

A path is text only where an engine command takes `"path": string`; a
name that is not valid Unicode is reported there instead of being opened
under a lossy rendering (another file, or a confusing "can't read").

- ui-egui: dialogs::unicode() and NotUnicode ("<path:?> is not valid
  Unicode"); dialogs::open_path(app, &Path), moved from the desktop app,
  is the one open-by-extension path for dropped, command-line and
  Workspace files.
- Services: the file dialogs return PathBuf (pick_save and pick_folder
  start from a &Path); callers convert where the text field or command
  takes the path.
- Sampler drops and Load Sample pass the path to mix.sampler_load
  through unicode(); the audio test checks the Path.
- Workspace panel keeps entry paths; names are shown lossily only as
  labels. workspace_dir stays a String (serialized preferences).
- soundcraft-cli: utf8() is unicode(), CliError::NonUnicodePath is
  NotUnicode; it is called only for info's session, --in, --bounce and
  --save. convert's output format test no longer treats a non-Unicode
  extension as "no extension".
- docs/cli.md: sessions and bounces the engine opens or writes must be
  valid Unicode.

Behaviour changes:
- A dropped, command-line, picked or Workspace file whose name is not
  valid Unicode is not opened: the status line (and log) says
  "<path:?> is not valid Unicode", with the bytes escaped. Dropped files
  used to be opened under a U+FFFD rendering.
- A file dialog result that is not valid Unicode leaves the path field
  unchanged and shows the same warning.
- Double-clicking a Workspace file opens it like a dropped file:
  .MID/.MIDI/.SCRAFT in any case open as MIDI/session (they used to
  import as audio), and a failed open is logged.
- convert OUT with an extension that is not valid Unicode is refused as
  an unsupported format instead of being written as WAV.

* fix(app): read Env after the launch flags; tidy Switch and Env

* fix(audio-io): decode every extension the format table lists

* fix(cli): write convert output atomically; tighten --connect and loopback

- convert writes every output to a new file in the output's folder, flushes it and renames it
  over the target, removing the new file on failure. A failed write (disk full, unwritable
  folder) no longer truncates the input under --in-place (RFC-0012 rule 1.6). A link output
  still writes the linked file.
- --save in place already goes through the engine's replace_file (tmp + rename), and
  file.bounce_mix renders to <name>.part and renames it when complete, so both are atomic.
- The desktop's --control / SOUNDCRAFT_CONTROL_PORT no longer treat [::1] as loopback: the
  control server binds 127.0.0.1 only. [::1]:P is a usage error from the flag and is ignored
  with a warning from the variable. The CLI's --connect / app --port still accept [::1]:P.
  Error tip and docs/cli.md updated.
- mcp --connect conflicts with --demo (exit 2): the demo session belongs to the in-process
  server. Help and docs/cli.md say so.

* test(audio-io): list the decode fixtures in ATTRIBUTION.md

cargo xtask assets needs a row for every committed audio file; one
directory row covers crates/audio-io/tests/fixtures/.

* fix(cli): refuse non-finite --start and --end values

* refactor(cli): parse the control target once; open sessions by path; keep error sources

- A1: ControlTarget { host, port } is parsed once by args::control_target; is_loopback() replaces
  control_port/ControlPort, and Remote::new takes &ControlTarget.
- A2: engine::io::open_session takes impl AsRef<Path>; the CLI no longer requires a Unicode path
  to open a session, only for the --bounce and --save paths that go into JSON.
- A3: ValueError::Json keeps the serde_json::Error; Remote::request returns RemoteError and
  AutomationRoots::new returns RootError, both carried by CliError.

* test(engine): pass session paths to open_session without a borrow

* test(cli): info opens a session at a non-Unicode path

* refactor(app): log the shared long version; info: session extension in any case; Switch beside Env
2026-10-27 21:09:43 -04:00
.cargo
.config ci: require nextest 0.9.150 2026-10-26 14:13:06 -06:00
.github refactor(cli): parse soundcraft-cli arguments with clap, following RFC-0012 (#1136) 2026-10-27 21:09:43 -04:00
apps refactor(cli): parse soundcraft-cli arguments with clap, following RFC-0012 (#1136) 2026-10-27 21:09:43 -04:00
assets/app-icon
contributors
crates refactor(cli): parse soundcraft-cli arguments with clap, following RFC-0012 (#1136) 2026-10-27 21:09:43 -04:00
docs refactor(cli): parse soundcraft-cli arguments with clap, following RFC-0012 (#1136) 2026-10-27 21:09:43 -04:00
lint/ast-grep refactor(cli): parse soundcraft-cli arguments with clap, following RFC-0012 (#1136) 2026-10-27 21:09:43 -04:00
packaging refactor(cli): parse soundcraft-cli arguments with clap, following RFC-0012 (#1136) 2026-10-27 21:09:43 -04:00
xtask
.coderabbit.yaml
.gitignore
AGENTS.md
ATTRIBUTION.md refactor(cli): parse soundcraft-cli arguments with clap, following RFC-0012 (#1136) 2026-10-27 21:09:43 -04:00
Cargo.lock refactor(cli): parse soundcraft-cli arguments with clap, following RFC-0012 (#1136) 2026-10-27 21:09:43 -04:00
Cargo.toml refactor(cli): parse soundcraft-cli arguments with clap, following RFC-0012 (#1136) 2026-10-27 21:09:43 -04:00
CLAUDE.md
clippy.toml fix(ui): dropdown menus grow back after a search with no matches (#1140) 2026-10-27 10:27:11 -04:00
LICENSE-APACHE
LICENSE-MIT
NOTICE Migrate SoundCraft to egui 0.37 (#1063) 2026-10-25 10:48:56 -04:00
README.md refactor(cli): parse soundcraft-cli arguments with clap, following RFC-0012 (#1136) 2026-10-27 21:09:43 -04:00
ROADMAP.md ui: Tracks and Groups and Clips in full-height dock columns (#1088) 2026-10-25 21:33:15 -04:00
rustfmt.toml
sonar-project.properties

ArtCraft

SoundCraft icon: a singing bird on a blue tile

SoundCraft

Recording, editing and mixing audio; an open-source, modern digital audio workstation (DAW), built in pure Rust.

A complete digital audio workstation that runs natively on macOS, Windows, Linux and FreeBSD, and in the browser. Multitrack editing, a full mixer with plugins, sends and automation, MIDI, recording and bouncing, and every action scriptable from the command line or by an AI agent.

Rust egui Platforms License: MIT OR Apache-2.0 Status: pre-alpha
Scanned by SonarCloud Reviewed by CodeRabbit

Join the ArtCraft community on Discord

SoundCraft on getartcraft.com · ArtCraft · All Crafting Apps

SoundCraft's Edit window with a nine-track session: drums, bass, pad, keys and lead, a reverb aux and a master fader
The Edit window: tracks, waveforms, MIDI, rulers, markers and the transport, playing the built-in demo song (every sound in it is synthesised by SoundCraft itself).

Note

ArtCraft is a community of artists from all walks of life. Painters, photographers, filmmakers, illustrators, designers, animators, hobbyists, and people who picked up a pencil last week. If you make things, you're one of us. Come say hi on Discord.

What it does · Getting started · Agents & scripts · How it's built · Status · Downloads · The Crafting Apps · License

What it does

SoundCraft follows the workflow audio engineers already know: an Edit window for arranging and editing, a Mix window for balancing, edit modes, tools, playlists, memory locations, AudioSuite processing and the rest. If you've spent years in that workflow, your hands should already know where things are.

The Mix window with channel strips, inserts, sends, I/O, pan knobs, faders and meters, and an EQ plugin window The MIDI editor showing a lead melody as a piano roll with a velocity lane
Mix window. Ten inserts and ten sends per track, busses and aux inputs, master faders, VCAs, routing folders, solo-in-place with implicit solo, and meters with peak hold. MIDI editor. Instrument tracks with built-in synths, a piano roll with a velocity lane, step input, quantize, transpose, and Standard MIDI File import and export.
Volume automation drawn on the Pad track, with the Memory Locations and Big Counter windows open The SoundCraft app icon: a singing nightingale on a teal field
Automation and markers. Breakpoint automation for volume, pan, mute, sends and every plugin parameter, written live in Write, Touch and Latch, plus memory locations and a big counter. Everywhere. Signed builds for macOS (universal), Windows (x64, x86, Arm), Linux (AppImage, deb, rpm, Flatpak), FreeBSD, and a WebAssembly build that runs in the browser.
The Video window showing a colour-bar test movie with a timecode burn-in, above a Video track with thumbnails in the Edit window The Renderer window showing a 7.1.4 speaker layout with two object tracks, beside the Mix window with surround panners and Object/Bed buttons
Video. A picture track with thumbnails and a Video window that follows the playhead, decoding H.264, ProRes and Motion JPEG in pure Rust, with timecode burn-in and sync offset. Surround and immersive. Main and bus formats from stereo to 9.1.6 and Ambisonics, a surround panner with divergence and height, object/bed routing and a Renderer window with live speaker levels.
The Score Editor showing the lead melody in standard notation on a treble and bass staff
Notation. A Score Editor for MIDI tracks, MusicXML export for Sibelius and other notation programs, and printable engraved scores. Your plugins too. CLAP, VST3 and Audio Units, with their own editors, their state saved in the session, and every instance created off the audio thread.

Feature tour

  • Editing: Shuffle, Slip, Spot and Grid modes; Smart, Selector, Grabber, Trim, Pencil, Zoom and Scrubber tools; cut, copy, paste, duplicate, repeat, shift, insert silence, separate (at selection, on grid, at transients), heal, consolidate, strip silence, nudge; fades and crossfades in five shapes, drawn straight from the clip corners; playlists with comping lanes; clip gain lines; edit groups; single-key Commands Focus shortcuts; trackpad scrolling that follows your fingers and pinch-to-zoom around the pointer; undo for everything.
  • Mixing: fader and pan with automation, pre/post sends, busses, aux inputs, master faders, VCA masters, routing folders, solo modes, mute and solo groups, phase invert and trim, clip effects, and automatic plugin delay compensation.
  • Automation: volume, pan, mute, send and every plugin parameter; Write, Touch, Latch and Trim passes recorded live from the faders; thin, glide, coalesce and convert to clip gain.
  • Plugins: 7-band and 1-band EQ, compressor/limiter, expander/gate, de-esser, maximizer, channel strip, room and plate reverbs, mod delay, auto pan, chorus, flanger, phaser, ring modulator, saturator, lo-fi, rectifier, subharmonic, pitch shifter, time shift, gain, trim, invert, DC removal, signal generator, dither, a subtractive synth, a drum synth and a sampler (plays any WAV, AIFF, FLAC, MP3 or OGG across the keyboard), all original. CLAP plugins load too. Any processor runs offline as AudioSuite, and user presets save per plugin.
  • MIDI: instrument and MIDI tracks, a piano-roll MIDI editor with a velocity lane, an event list, a score view, step input, quantize, transpose, real-time properties, Standard MIDI Files, and Audio-to-MIDI from a sung or played melody.
  • Recording: punch in on a selection or on the fly, pre/post-roll, loop recording with a playlist per take, input monitoring through the full channel strip, and autosave with recovery.
  • Time and tempo: Bars|Beats, Min:Secs, Timecode (every rate, drop-frame included), Feet+Frames and Samples; tempo and meter maps with a tempo editor; linear, parabolic and S-curve tempo ramps; key signatures and detected chords; Beat Detective and Identify Beat; pitch-preserving Elastic warping.
  • Audio files: WAV/BWF/RF64, AIFF/AIFC and FLAC in and out; MP3, Ogg Vorbis, AAC, ALAC, CAF and the audio of MP4/MOV movies in. Any sample rate and bit depth.
  • Bounce: mix or stems to WAV, AIFF or FLAC at 16/24/32-bit float with optional dither and normalisation, reporting peak, true peak and integrated loudness (LUFS).
  • Fast: a parallel mix engine, copy-on-write undo, and a UI that holds 120 frames per second with seventy-odd tracks playing.

Getting started

Download a build from the releases page, or build from source with Rust 1.95 or later for the desktop/web UI (the core workspace's declared minimum remains Rust 1.90):

git clone https://github.com/storytold/soundcraft
cd soundcraft
cargo run --release -p soundcraft -- --demo     # opens the demo song

Linux needs the ALSA headers (libasound2-dev on Debian/Ubuntu, alsa-lib-devel on Fedora).

The web build:

cd apps/soundcraft-web && trunk serve --release   # then open http://127.0.0.1:8080

Useful shortcuts: Space play/stop, ⌘= Mix/Edit, F1–F4 edit modes, F5–F10 tools, ⌘E separate, ⌘D duplicate, Enter new marker, ⌘⇧N new tracks. Setup › Keyboard Shortcuts lists them all.

Logs

The desktop app writes its log records to standard error and to logs/soundcraft.log in the settings directory, beside ui.json, Autosave/ and Presets/ (Linux ~/.config/soundcraft/logs/, or $XDG_CONFIG_HOME/soundcraft/logs/; macOS ~/Library/Application Support/SoundCraft/logs/; Windows %APPDATA%\SoundCraft\logs\). A start from a desktop menu or the Dock has no terminal, so this file is what to attach to a bug report: the engine's panic report, audio devices that failed to open or broke, plugins that refused their stored state, CLAP plugins' own messages and files that failed to open land there. Each launch moves the previous log to soundcraft.1.log (and that one to soundcraft.2.log), so the log of a run that crashed survives the next start. The file stops growing at 16 MiB. --version writes no file, and runs with SOUNDCRAFT_NO_PREFS log to standard error only.

Variable Effect
RUST_LOG Log levels for standard error and the log file. Default: info for SoundCraft's own crates, warn for everything else. env_logger-style directives replace that, e.g. RUST_LOG=debug, RUST_LOG=warn,soundcraft_mix=trace or RUST_LOG=info,wgpu_core=warn; a directive ending in * covers every target starting with it (soundcraft*=debug).
SOUNDCRAFT_NO_PREFS 1: no preferences read or written and no log file (agents' test runs).

The realtime audio thread never writes a record itself: the logger keeps the first one it logs (without waiting or allocating), counts the rest, and the UI thread writes them. The logger is apps/soundcraft/src/logging.rs. The web app logs to the browser console instead.

For agents and scripts

Everything you can click is also a command with an id and JSON parameters, and the same commands are reachable from the menus, the keyboard, the command line, a JSON control channel and an MCP server. Programmatic calls never open dialogs, and session.inspect reports exactly what changed, so an agent can check its own work.

# Headless: load the demo, turn the kick down, add an EQ, bounce.
soundcraft-cli run --demo \
  --cmd 'mix.volume={"track":"Kick","db":-6}' \
  --cmd 'mix.insert={"track":"Bass","plugin":"eq_7band"}' \
  --bounce mix.wav

# Drive the running app.
soundcraft --demo --control 7801 &
soundcraft-cli app --port 7801 transport.play
soundcraft-cli app --port 7801 ui.screenshot '{"path":"shot.png"}'

# Give Claude a DAW.
claude mcp add soundcraft -- soundcraft-cli mcp --demo

# Or keep the agent to some folders (reads and writes are separate roots).
soundcraft-cli mcp --automation-read-root DIR --automation-write-root DIR

See docs/cli.md for the command-line tool (subcommands, exit codes, environment variables, man pages and completions), docs/control-protocol.md and docs/mcp.md. soundcraft-cli commands lists every command.

Open SoundCraft › Session Audio Health to check loaded audio availability, sample-rate mismatches and clip source bounds across all playlists, including alternate takes. The report offers a refresh button and suggests how to resolve each issue. Scripts can obtain the same JSON report with soundcraft-cli run --demo --cmd 'session.audio_health={}'. This checks loaded media, not files on disk, signal levels, plugins or routing.

How it's built

SoundCraft is a Cargo workspace of small crates with strict layering: nothing below the UI knows about egui, so the interface could be swapped for another one.

Crate What it does
soundcraft-time Timebases, tempo and meter maps, timecode, grid
soundcraft-audio-io Audio file formats and waveform overviews
soundcraft-midi Standard MIDI Files and MIDI operations
soundcraft-dsp Plugins, offline processing, meters, loudness, FFT
soundcraft-model The session document
soundcraft-mix The mix engine (realtime and offline share it)
soundcraft-engine Commands, undo, editing, import/export, bounce
soundcraft-playback Audio devices (CoreAudio, WASAPI, ALSA, WebAudio) and recording
soundcraft-automation MCP server and control-channel client
soundcraft-ui-egui The user interface

The code never panics on bad input: errors are values, and a malformed file or a bad agent call produces a message, never a crash. cargo xtask ci runs formatting, clippy, the tests, the asset and layering checks and the WebAssembly build.

Status and roadmap

SoundCraft is pre-alpha: it covers 94 % of the incumbent's menu items (measured), about 70 % of its features by presence, and is roughly 40 % of the way to replacing it for real work: record, edit, mix and bounce work end to end, but choosing an audio interface and buffer size, recording MIDI from a controller and session interchange (AAF/OMF) are still missing, and those are the alpha checklist. Translations are missing too. The status, the stage and our effort estimates are in ROADMAP.md; the full assessment is in docs/target-app-parity.md, the ranked list of what's missing in docs/gaps.md, and the menu-by-menu comparison in docs/parity-checklist.md. Bug reports and wish lists are very welcome, in the issues or on Discord.

Downloads

Download SoundCraft from GitHub: the latest release has every build listed below, and all releases have earlier versions and their notes. <ver> in the file names is the version number, and SHA256SUMS.txt lists a checksum for every file.

Windows

Build Installer Portable
x64 (64-bit Intel/AMD) soundcraft-<ver>-windows-x64.msi soundcraft-<ver>-windows-x64-portable.zip
arm64 (Snapdragon and other ARM PCs) soundcraft-<ver>-windows-arm64.msi soundcraft-<ver>-windows-arm64-portable.zip
x86 (32-bit) soundcraft-<ver>-windows-x86.msi soundcraft-<ver>-windows-x86-portable.zip

Installers and executables are code-signed.

If the app doesn't open on Windows: the desktop app initializes only DirectX 12 by default. Letting wgpu also create an OpenGL instance can crash some graphics drivers (AMD's atio6axx.dll) before the window appears, so the app would flash in Task Manager and quit. WGPU_BACKEND overrides the default for troubleshooting (for example dx12 or vulkan). In PowerShell, from the folder containing the executable:

$env:WGPU_BACKEND = "vulkan"
& .\soundcraft.exe
Remove-Item Env:WGPU_BACKEND                     # restore the default for later launches

An explicit gl override can bring the driver crash back on affected systems. The macOS, Linux and web backend defaults are unchanged.

DirectX 12 shaders are compiled with FXC, which is part of Windows, so no dxcompiler.dll or dxil.dll is needed ("Parent device is lost" at start, #185). $env:WGPU_DX12_COMPILER = "dxc" (or "auto") selects wgpu's DXC handling again for troubleshooting.

macOS

Build File Notes
App, universal (Apple silicon + Intel) soundcraft-<ver>-macos-universal.dmg Signed and notarized
Command-line tool, universal soundcraft-cli-<ver>-macos-universal.zip Signed and notarized

Linux

Format x86_64 aarch64 (ARM64) Notes
AppImage soundcraft-<ver>-linux-x86_64.AppImage soundcraft-<ver>-linux-aarch64.AppImage Runs anywhere; updates itself with AppImageUpdate (.zsync files)
Flatpak soundcraft-<ver>-linux-x86_64.flatpak soundcraft-<ver>-linux-aarch64.flatpak Sandboxed; flatpak install --user <file>
Debian/Ubuntu soundcraft-<ver>-linux-x86_64.deb soundcraft-<ver>-linux-aarch64.deb
Fedora/RHEL/openSUSE soundcraft-<ver>-linux-x86_64.rpm soundcraft-<ver>-linux-aarch64.rpm
Tarball soundcraft-<ver>-linux-x86_64.tar.gz soundcraft-<ver>-linux-aarch64.tar.gz Unpack anywhere

RISC-V (riscv64): soundcraft-<ver>-linux-riscv64.tar.gz, a tarball only (glibc 2.39+, e.g. Ubuntu 24.04).

FreeBSD

Build File
x86_64 soundcraft-<ver>-freebsd-x86_64.tar.gz

Web (WebAssembly)

Build File Notes
Static site soundcraft-web-<ver>.zip Runs in a modern browser; host it on any static server

The Crafting Apps

SoundCraft is one of the Crafting Apps: free, open-source creative tools from the ArtCraft team, each written from scratch in Rust and each able to stand on its own.

App What it's for Code Learn more
PhotoCraft Image editing: layers, masks, type and real PSD files GitHub Website
VectorCraft Vector illustration GitHub Website
FilmCraft Video editing, color and sound GitHub Website
LightCraft Photo library and raw development GitHub Website
PdfCraft Reading, organizing and protecting PDFs GitHub Website
EffectCraft Motion graphics and visual effects GitHub Website
DesignCraft Page layout and publishing GitHub Website
SoundCraft Recording, editing and mixing audio · you are here GitHub Website

And ArtCraft itself, our AI image and video studio for artists who want real control.


Join the ArtCraft community on Discord

Come make things with us

Our Discord is where artists of every kind hang out: people who paint, shoot, draw, cut film, set type, and people still figuring out what they like to make. Share what you're working on, ask for help, tell us what's broken, or tell us what you wish these tools could do. Whatever your medium and however long you've been at it, you're welcome here.

discord.gg/artcraft · getartcraft.com · The Crafting Apps · SoundCraft

License and credits

SoundCraft is dual-licensed under MIT or Apache-2.0, at your option. Copyright (c) 2026 ArtCraft Team and the SoundCraft contributors. Required notices are in NOTICE.

Bundled fonts, icons, images and other assets keep their own open licenses; each one is listed with its author, source and license in ATTRIBUTION.md.

Every icon in the interface is drawn in code, the app icon is original, and the demo song and the screenshots' audio are synthesised by SoundCraft itself, so there are no samples or artwork from anyone else in this repository.

The ArtCraft name, wordmark and logos in docs/brand/ are trademarks of the ArtCraft Team and are not covered by this license. They may be used only unmodified, and only as part of this repository and SoundCraft, under docs/brand/LICENSE-brand.txt. Forks and modified versions must remove them.

ArtCraft
Made by the ArtCraft team and community.

Tracks and Groups, Clips, MIDI Editor, Memory Locations, and Undo History use movable panels. Edit and Mix retain separate arrangements; Clips and MIDI Editor belong to Edit. Transport, plug-in, and specialist utility windows retain their existing hosts. Drag a panel header to a target edge or another header. Its context menu offers Float Panel, Move Panel to Group, and Close Panel; the tab group's menu adds Float Tab Group and Close Tab Group. Existing panel toggles reopen panels at their saved location; Reset Panel Layout restores the app layout. Floating panels remain inside the current app window.