mirror of
https://github.com/storytold/soundcraft.git
synced 2026-10-24 18:46:49 -06:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4410486fc2 | ||
|
|
330a698397 | ||
|
|
3bc2d92b4e | ||
|
|
e5b5f9907f | ||
|
|
0d3228ef9c | ||
|
|
f9d0d26e5b | ||
|
|
f68f737b3d | ||
|
|
dba0bea03d | ||
|
|
e77503a815 | ||
|
|
3a4f18647f | ||
|
|
af0174942a | ||
|
|
dc2ffda4d6 | ||
|
|
9001c76b56 | ||
|
|
3364f7ca30 | ||
|
|
f6deacbf9a | ||
|
|
d034c8a17b | ||
|
|
d03eba8648 | ||
|
|
1fa02e5499 | ||
|
|
5033eb8408 | ||
|
|
c4c48d8527 | ||
|
|
9ef16e1e6c | ||
|
|
30347ee42f | ||
|
|
d07624da1f | ||
|
|
82340cee5c | ||
|
|
3b9d5b9423 | ||
|
|
be859f6cc5 | ||
|
|
b92370d32f | ||
|
|
94e13543ec | ||
|
|
0734281104 | ||
|
|
d3348bcbbc | ||
|
|
1f5bc965fe |
@@ -250,6 +250,76 @@ jobs:
|
||||
path: dist/release/*.flatpak
|
||||
if-no-files-found: error
|
||||
|
||||
linux-riscv64:
|
||||
name: Linux riscv64
|
||||
needs: version
|
||||
# Cross-compiled on x86_64 (no hosted RISC-V runners) against the runner's own Ubuntu release
|
||||
# from ports.ubuntu.com, so the binaries need that release's glibc (24.04: 2.39). 22.04's
|
||||
# riscv64 ports lag its amd64 updates, which breaks the multiarch install. tar.gz only; the
|
||||
# CLI (which links no ALSA) is smoke-tested under QEMU. Plugin hosting (CLAP, VST3) is
|
||||
# pure-Rust bindings plus dlopen, so it builds here unchanged; cpal uses ALSA (no JACK).
|
||||
runs-on: ubuntu-24.04
|
||||
# No `environment: release`: this job signs nothing, so it gets no release secrets.
|
||||
env:
|
||||
SOUNDCRAFT_VERSION: ${{ needs.version.outputs.version }}
|
||||
SOUNDCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: riscv64gc-unknown-linux-gnu
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
key: release-linux-riscv64
|
||||
- name: Apply version override
|
||||
if: inputs.version != ''
|
||||
run: cargo xtask version set "$SOUNDCRAFT_VERSION"
|
||||
- name: Install cross-compilation dependencies
|
||||
run: |
|
||||
. /etc/os-release
|
||||
sudo dpkg --add-architecture riscv64
|
||||
# Pin the existing (archive.ubuntu.com) sources to amd64; riscv64 comes from ports.
|
||||
if [ -f /etc/apt/sources.list.d/ubuntu.sources ]; then
|
||||
sudo sed -i '/^Types: deb/a Architectures: amd64' /etc/apt/sources.list.d/ubuntu.sources
|
||||
fi
|
||||
if [ -f /etc/apt/sources.list ]; then
|
||||
sudo sed -i 's/^deb /deb [arch=amd64] /g' /etc/apt/sources.list
|
||||
sudo sed -i 's/^deb-src /deb-src [arch=amd64] /g' /etc/apt/sources.list
|
||||
fi
|
||||
for suite in "$VERSION_CODENAME" "$VERSION_CODENAME-updates" "$VERSION_CODENAME-security"; do
|
||||
echo "deb [arch=riscv64] http://ports.ubuntu.com/ubuntu-ports $suite main universe restricted multiverse"
|
||||
done | sudo tee /etc/apt/sources.list.d/riscv64.list
|
||||
sudo apt-get update
|
||||
# The riscv64 twins of the linux job's -dev packages; libasound2-dev is cpal's ALSA backend.
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
gcc-riscv64-linux-gnu binutils-riscv64-linux-gnu libc6-dev-riscv64-cross libc6-dev:riscv64 qemu-user \
|
||||
pkg-config file desktop-file-utils appstream \
|
||||
libxkbcommon-dev:riscv64 libwayland-dev:riscv64 libx11-dev:riscv64 libxrandr-dev:riscv64 \
|
||||
libxi-dev:riscv64 libgl1-mesa-dev:riscv64 libasound2-dev:riscv64
|
||||
- name: Build and package (tar.gz)
|
||||
env:
|
||||
CARGO_TARGET_RISCV64GC_UNKNOWN_LINUX_GNU_LINKER: riscv64-linux-gnu-gcc
|
||||
PKG_CONFIG_SYSROOT_DIR: /
|
||||
PKG_CONFIG_PATH: /usr/lib/riscv64-linux-gnu/pkgconfig:/usr/share/pkgconfig
|
||||
PKG_CONFIG_ALLOW_CROSS: "1"
|
||||
CROSS_ARCH: riscv64
|
||||
CROSS_TARGET: riscv64gc-unknown-linux-gnu
|
||||
CROSS_COMPILE: riscv64-linux-gnu-
|
||||
EMULATOR: qemu-riscv64
|
||||
QEMU_LD_PREFIX: /usr/riscv64-linux-gnu
|
||||
run: packaging/linux/package.sh --formats tar
|
||||
- name: Check the package
|
||||
run: |
|
||||
file target/riscv64gc-unknown-linux-gnu/release/soundcraft | grep -q 'RISC-V'
|
||||
# cpal's ALSA backend: the one audio library linked at build time (see nfpm.yaml).
|
||||
riscv64-linux-gnu-readelf -d target/riscv64gc-unknown-linux-gnu/release/soundcraft | grep -q 'libasound'
|
||||
tar -tzf "dist/release/soundcraft-$SOUNDCRAFT_VERSION-linux-riscv64.tar.gz" | grep -E 'bin/soundcraft$'
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: release-linux-riscv64
|
||||
path: dist/release/*.tar.gz
|
||||
if-no-files-found: error
|
||||
|
||||
freebsd:
|
||||
name: FreeBSD x86_64
|
||||
needs: version
|
||||
@@ -328,7 +398,7 @@ jobs:
|
||||
release:
|
||||
name: Draft GitHub Release
|
||||
# Needs the signed jobs, which only run on the `release` branch: a branch dispatch never drafts.
|
||||
needs: [version, macos, windows, linux, flatpak, freebsd, web]
|
||||
needs: [version, macos, windows, linux, linux-riscv64, flatpak, freebsd, web]
|
||||
runs-on: ubuntu-latest
|
||||
environment: release
|
||||
permissions:
|
||||
|
||||
@@ -6,7 +6,7 @@ Standards and learnings shared across the crafting apps live in `../../craftrule
|
||||
|
||||
## Start every session here
|
||||
1. Read `plan/STATUS.md` (current milestone, next task) and `plan/execution-plan.md`. `plan/` is gitignored (local only). Behaviour reference: `plan/protools/` (observed menu tree `menus-clean.txt` and screenshots; never committed).
|
||||
2. Read `ROADMAP.md` (committed) for status, milestones and the parity estimate.
|
||||
2. Read `ROADMAP.md` (committed) for stage, status and the parity numbers; pick work from `docs/gaps.md`. The progress docs follow craftrules `standards/progress-docs.md`: `docs/target-app-parity.md` (assessment), `docs/gaps.md`, `docs/roadmap.md`, `docs/ui-parity.md`, `docs/file-format-parity.md`, `docs/hardware-parity.md`, `docs/plugin-parity.md`, `docs/localization-parity.md`; `docs/parity-checklist.md` is generated. Update their timestamps and revision history when you change them.
|
||||
3. Work autonomously; don't stop to ask unless a decision is genuinely the user's.
|
||||
|
||||
## ⚠️ Assets: the absolute rule
|
||||
@@ -51,7 +51,7 @@ Nothing below L6 depends on egui/eframe/winit/rfd. The UI is thin: panels read e
|
||||
## Everything is a command
|
||||
User-visible behaviour = a command in `crates/engine/src/cmd/*` (id, label, menu path, shortcut, params doc, `enabled`, `run`) + tests. UI-only commands (windows, dialogs) live in `crates/ui-egui/src/menus.rs` (`UI_COMMANDS`). Menus, shortcuts, the CLI, the control channel and MCP all dispatch the same ids. Programmatic calls never open dialogs; only menu-style invocation (`ui.menu.invoke`) does. New commands go in a new module with its own `specs()`, registered by one line in `cmd/mod.rs`.
|
||||
|
||||
Parity is measured, not guessed: `crates/engine/catalog/menus.txt` lists the incumbent's menu leaves (names only). `cargo xtask parity` writes `docs/parity.md`; a test enforces a floor that only rises.
|
||||
Parity is measured, not guessed: `crates/engine/catalog/menus.txt` lists the incumbent's menu leaves (names only). `cargo xtask parity` writes `docs/parity-checklist.md` (generated; the hand-written assessment is `docs/target-app-parity.md`); a test enforces a floor that only rises.
|
||||
|
||||
## Running and looking at the app
|
||||
- `cargo run --release -p soundcraft -- --demo --control 0` (demo session + control channel on a free port; the port is printed).
|
||||
@@ -66,10 +66,10 @@ Parity is measured, not guessed: `crates/engine/catalog/menus.txt` lists the inc
|
||||
`cargo xtask ci` = fmt check, `clippy --workspace --all-targets -D warnings`, tests, assets, layers, wasm check. Commit after each arc of work that builds; one topic per commit.
|
||||
|
||||
## Release
|
||||
Push to the `release` branch to build signed installers for macOS (universal DMG), Windows (x64/x86 MSI + zip), Linux (AppImage, deb, rpm, tar.gz, Flatpak manifest), FreeBSD and Web (wasm zip). Version: `cargo xtask version [set X.Y.Z]`. See `docs/release-playbook.md` and craftrules `release/playbook.md`.
|
||||
Push to the `release` branch to build signed installers for macOS (universal DMG), Windows (x64/x86 MSI + zip), Linux (AppImage, deb, rpm, tar.gz, Flatpak; riscv64 tar.gz), FreeBSD and Web (wasm zip). Version: `cargo xtask version [set X.Y.Z]`. See `docs/release-playbook.md` and craftrules `release/playbook.md`.
|
||||
|
||||
## Roadmap
|
||||
`ROADMAP.md` (committed) tracks status, milestones and estimates. Update it whenever a milestone lands.
|
||||
`ROADMAP.md` (committed) is the one-page summary (stage, numbers, dimensions, languages, progress log); milestones live in `docs/roadmap.md`. Update both whenever a milestone lands, and close or shrink the matching `docs/gaps.md` entry.
|
||||
|
||||
## Contributor credits (About window)
|
||||
|
||||
|
||||
Generated
+18
-55
@@ -837,23 +837,6 @@ version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f"
|
||||
|
||||
[[package]]
|
||||
name = "dark-light"
|
||||
version = "3.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f66b71ac6b73812b0bf51cda8dd646a268fbb38623f45b2b7907593c323f745"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"objc2 0.6.5",
|
||||
"objc2-foundation 0.3.2",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
"windows-sys 0.61.2",
|
||||
"winreg",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dasp_sample"
|
||||
version = "0.11.0"
|
||||
@@ -1319,15 +1302,6 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.34"
|
||||
@@ -3429,7 +3403,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "soundcraft"
|
||||
version = "0.4.0"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"eframe",
|
||||
"egui",
|
||||
@@ -3445,7 +3419,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "soundcraft-au-host"
|
||||
version = "0.4.0"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"serde",
|
||||
@@ -3455,7 +3429,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "soundcraft-audio-io"
|
||||
version = "0.4.0"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"proptest",
|
||||
@@ -3467,7 +3441,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "soundcraft-automation"
|
||||
version = "0.4.0"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"serde_json",
|
||||
@@ -3477,7 +3451,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "soundcraft-clap-host"
|
||||
version = "0.4.0"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"clap-sys",
|
||||
"libloading",
|
||||
@@ -3489,7 +3463,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "soundcraft-cli"
|
||||
version = "0.4.0"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
"soundcraft-audio-io",
|
||||
@@ -3500,7 +3474,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "soundcraft-dsp"
|
||||
version = "0.4.0"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"proptest",
|
||||
@@ -3511,7 +3485,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "soundcraft-engine"
|
||||
version = "0.4.0"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"proptest",
|
||||
@@ -3532,7 +3506,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "soundcraft-midi"
|
||||
version = "0.4.0"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"proptest",
|
||||
"serde",
|
||||
@@ -3542,7 +3516,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "soundcraft-mix"
|
||||
version = "0.4.0"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"rayon",
|
||||
@@ -3558,7 +3532,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "soundcraft-model"
|
||||
version = "0.4.0"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"proptest",
|
||||
"serde",
|
||||
@@ -3571,7 +3545,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "soundcraft-playback"
|
||||
version = "0.4.0"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"cpal",
|
||||
"log",
|
||||
@@ -3584,7 +3558,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "soundcraft-time"
|
||||
version = "0.4.0"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"proptest",
|
||||
"serde",
|
||||
@@ -3594,9 +3568,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "soundcraft-ui-egui"
|
||||
version = "0.4.0"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"dark-light",
|
||||
"egui",
|
||||
"egui_kittest",
|
||||
"image",
|
||||
@@ -3620,7 +3593,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "soundcraft-video"
|
||||
version = "0.4.0"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"image",
|
||||
"proptest",
|
||||
@@ -3631,7 +3604,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "soundcraft-vst3-host"
|
||||
version = "0.4.0"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"libloading",
|
||||
"log",
|
||||
@@ -3646,7 +3619,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "soundcraft-web"
|
||||
version = "0.4.0"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"eframe",
|
||||
"egui",
|
||||
@@ -5106,16 +5079,6 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
version = "0.56.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winresource"
|
||||
version = "0.1.31"
|
||||
@@ -5203,7 +5166,7 @@ checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7"
|
||||
|
||||
[[package]]
|
||||
name = "xtask"
|
||||
version = "0.4.0"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
+1
-2
@@ -4,7 +4,7 @@ members = ["crates/*", "apps/*", "xtask"]
|
||||
default-members = ["crates/*", "apps/*", "xtask"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.4.0"
|
||||
version = "0.5.0"
|
||||
edition = "2024"
|
||||
license = "MIT OR Apache-2.0"
|
||||
rust-version = "1.90"
|
||||
@@ -44,7 +44,6 @@ cpal = "0.16"
|
||||
egui = "0.36"
|
||||
eframe = { version = "0.36", default-features = false }
|
||||
egui_kittest = "0.36"
|
||||
dark-light = "3"
|
||||
rfd = "0.17"
|
||||
image = { version = "0.25", default-features = false, features = ["png"] }
|
||||
|
||||
|
||||
@@ -236,10 +236,15 @@ and layering checks and the WebAssembly build.
|
||||
|
||||
## Status and roadmap
|
||||
|
||||
SoundCraft is **pre-alpha, and close to its first alpha**: it covers 94 % of the incumbent's menu
|
||||
items and roughly two thirds of its features in depth. The core works and is fun to use, and plenty is still missing. The
|
||||
honest status, what's next and our effort estimates are in [`ROADMAP.md`](ROADMAP.md); the
|
||||
menu-by-menu comparison is in [`docs/parity.md`](docs/parity.md). Bug reports and wish lists are
|
||||
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`](ROADMAP.md); the full assessment is in
|
||||
[`docs/target-app-parity.md`](docs/target-app-parity.md), the ranked list of what's missing in
|
||||
[`docs/gaps.md`](docs/gaps.md), and the menu-by-menu comparison in
|
||||
[`docs/parity-checklist.md`](docs/parity-checklist.md). Bug reports and wish lists are
|
||||
very welcome, in the issues or on Discord.
|
||||
|
||||
## Downloads
|
||||
@@ -288,6 +293,8 @@ and web backend defaults are unchanged.
|
||||
| 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 |
|
||||
|
||||
+127
-55
@@ -1,54 +1,114 @@
|
||||
# SoundCraft roadmap
|
||||
|
||||
**Stage: pre-alpha** · next: alpha, ~5% and ~110–180 h away (the alpha gate's three failing workflows)
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** minor (readiness by audience with hours to ~95 %) · **Target:** Avid Pro Tools Ultimate 2026.4.1
|
||||
|
||||
SoundCraft is a clean-room, pure-Rust digital audio workstation that aims at full parity with
|
||||
Avid Pro Tools, and then beyond it: faster, open, scriptable and agent-controllable. This file is
|
||||
the honest status: what works today, what is missing, and how far we are.
|
||||
Avid Pro Tools, and then beyond it: faster, open, scriptable and agent-controllable. This page is
|
||||
the one-page summary; the detail is in [`docs/target-app-parity.md`](docs/target-app-parity.md)
|
||||
(assessment), [`docs/gaps.md`](docs/gaps.md) (ranked work list), [`docs/roadmap.md`](docs/roadmap.md)
|
||||
(milestones and beta plan) and [`docs/parity-checklist.md`](docs/parity-checklist.md) (generated
|
||||
menu checklist).
|
||||
|
||||
## Where we are (2026-10-07)
|
||||
## Headline numbers
|
||||
|
||||
| Measure | Value | How |
|
||||
| Number | Value | Kind |
|
||||
|---|---|---|
|
||||
| Menu-catalog parity (engine + UI) | **482 / 512 menu items (94 %)**; engine alone 397 / 512, see [`docs/parity.md`](docs/parity.md) | `cargo xtask parity` compares the incumbent's 512 menu leaves (names observed black-box) with our command registry. Most of what is left is Avid's online services (sign-in, cloud projects, collaboration, Splice, Learn), Dolby Atmos-specific items, HEAT and ARA |
|
||||
| Estimated overall feature parity | **~65 %** | judgement across the areas below, weighted by how much working engineers rely on them. Menu items are only the surface; depth (editing feel, plugin catalogue, MIDI and score editing, video) is where the gap is |
|
||||
| Distance to an **alpha** | **~85 % there, ~12–18 wall-clock hours** of Claude Opus 5.5 work | see [Alpha](#alpha) below |
|
||||
| Distance to **100 % parity** | **~110–150 wall-clock hours** of Claude Opus 5.5 work with a single agent; roughly 40–55 hours with 3–4 agents in parallel (the last three features in this session were built in parallel this way) | sum of the per-area estimates below |
|
||||
| Menu breadth | **482 / 512 Pro Tools menu leaves (94.1 %)**; engine alone 397 / 512 | measured (`cargo xtask parity` + UI-layer aliases) |
|
||||
| Feature breadth | **~70 %** | estimated (menus plus plugins, hardware, formats, immersive, cloud) |
|
||||
| **Ready for real work** | **~40 %** | estimated, weighted by dimension |
|
||||
| Mainstream practitioner (tracking/mixing engineer) | **~36 %** | estimated: 50.6 % weekly-area depth × 0.90 interaction × 0.92 stability × 0.85 file exchange ([how](docs/target-app-parity.md#mainstream-practitioner-and-essentials-user)) |
|
||||
| Essentials user (song or podcast, defaults) | **~57 %** | estimated: 72.4 % core-feature depth × 0.90 launch/stability × 0.90 discoverability × 0.97 files ([how](docs/target-app-parity.md#mainstream-practitioner-and-essentials-user)) |
|
||||
| Remaining to alpha | **~110–180 Opus 5.5 agent-hours** (the failing gate rows) | estimated, calibrated on PR history |
|
||||
| Remaining to beta | **~350–570 Opus 5.5 agent-hours** (includes the alpha work; ~60–70 % parallelizable) | estimated |
|
||||
| Remaining to full parity | **~950–1,450 Opus 5.5 agent-hours** | estimated |
|
||||
|
||||
### Area by area
|
||||
| Audience | Ready % | Agent-hours to ~95 % | Work that dominates |
|
||||
|---|---:|---:|---|
|
||||
| Full target (ready for real work) | ~40 % | ~950–1,450 h | Hardware, the stock plugin catalogue, AAF/OMF and immersive, localization, fidelity |
|
||||
| Mainstream practitioner | ~36 % | ~380–600 h | Interface and MIDI recording, AAF/OMF, stock-plugin depth, core-path bugs |
|
||||
| Essentials user | ~57 % | ~110–200 h | Launch and platform fixes, onboarding, effect panels and presets, MP3/AAC export |
|
||||
|
||||
| Area | Status | Parity | Remaining (h) |
|
||||
|---|---|---:|---:|
|
||||
| Session model, save/load, undo | Tracks, playlists, clips, fades, clip gain, automation, markers, groups, busses, video; JSON `.scraft` + Audio Files folder; plugin state; autosave; copy-on-write undo with gesture coalescing | 85 % | 8 |
|
||||
| Audio file I/O | WAV/BWF/RF64, AIFF/AIFC, FLAC (read/write); MP3, OGG, AAC, ALAC, CAF (read); peaks. Whole files are decoded into memory: disk streaming for very long sessions is still to do | 70 % | 12 |
|
||||
| Editing | Slip/Shuffle/Spot/Grid, cut/copy/paste/clear/duplicate/repeat/shift, Paste Special (merge MIDI, markers, clip gain/effects), separate/heal/trim/consolidate/strip silence, fades & crossfades, nudge, playlists with comping, every tool gesture, edit groups, clip groups (and `.scgrp` import/export) | 75 % | 20 |
|
||||
| Mixer & routing | Faders, pan, mute/solo (SIP, implicit solo), sends pre/post, busses, aux inputs, master faders, VCAs, routing folders, 10 inserts, plugin delay compensation, clip effects, preamp/instrument/object views | 75 % | 15 |
|
||||
| Automation | Breakpoint lanes for volume/pan/mute/sends/plugin params, trim automation; live Write/Touch/Latch passes; write/thin/glide/convert/coalesce. Surround-pan automation is still to do | 60 % | 15 |
|
||||
| Plugins (built-in) | 26 original processors + 2 instruments, AudioSuite offline processing for the whole AudioSuite menu | 45 % | 30 |
|
||||
| Third-party plugins | CLAP, VST3 and Audio Units (macOS): audio, parameters, latency, notes, state saved in sessions, instances created/destroyed off the audio thread, editors (floating CLAP GUIs, VST3 views on macOS, verified with Melodyne). Missing: AU editors, embedded CLAP GUIs, VST3 editors on Windows/Linux, ARA | 80 % | 10 |
|
||||
| MIDI & notation | MIDI/instrument tracks, SMF import/export, MIDI editor (piano roll, velocity, rubber-band select, keyboard transpose/move), event list, step input, quantize/transpose/velocity/duration, real-time properties, Score Editor, MusicXML export (Sibelius) and printable SVG score | 55 % | 20 |
|
||||
| Recording | Input capture, punch in/out (selection, on the fly, pre/post-roll), loop record into playlists, input monitoring through the channel strip, autosave and recovery | 60 % | 10 |
|
||||
| Elastic Audio / TCE / Beat Detective | Pitch-preserving warp on Elastic tracks, TCE to timeline, conform to tempo, Beat Detective and Identify Beat windows; audio-to-MIDI pitch detection | 45 % | 15 |
|
||||
| Surround | Main and bus formats from stereo to 9.1.6 and Ambisonics, surround panner with divergence/centre/LFE/height, ITU fold-down, multichannel bounce, Renderer window, object/bed routing | 60 % | 12 |
|
||||
| Video | Picture track with thumbnails, Video and Video Universe windows, H.264 (Baseline/Main/High), ProRes (all flavours) and Motion JPEG decoders in pure Rust, sync offset, relinking. Missing: HEVC/AV1/DNx, timecode-track placement, video export | 55 % | 15 |
|
||||
| UI fidelity | Edit + Mix windows, toolbar, rulers, track headers, menus for the whole catalog (macOS system menu bar), floating windows, dialogs (every file command has a path prompt), command palette | 70 % | 20 |
|
||||
| Agent control | CLI, JSON control channel, MCP server (headless + bridged), offscreen UI renders | 90 % (ahead of the incumbent) | 3 |
|
||||
| Release engineering | Signed macOS universal, Windows x64/x86, Linux AppImage/deb/rpm/tar/Flatpak, FreeBSD, Web/WASM on every push to `release` | 80 % | 3 |
|
||||
Hours are Opus 5.5 agent wall-clock hours, calibrated on this repo's PRs; 60–70 % parallelizes.
|
||||
Detail: [target-app-parity.md](docs/target-app-parity.md#readiness-by-audience).
|
||||
|
||||
## Alpha
|
||||
**Why pre-alpha:** the core-workflow gate in [docs/roadmap.md](docs/roadmap.md#alpha-gate) fails.
|
||||
Editing, mixing, bouncing, and saving and reopening work end to end. But a Pro Tools user can't
|
||||
record from a chosen audio interface at a chosen buffer size (only the default devices, a fixed
|
||||
512-sample block, and takes held in RAM). They can't record MIDI from a keyboard (there is no
|
||||
MIDI device layer). And they can't bring a session in or hand one back (no AAF/OMF/`.ptx`).
|
||||
Readiness (~40 %) sits right at the alpha bar, but a gate failure keeps the app pre-alpha. The
|
||||
2026-10-07 estimate ("~65 % overall, 110–150 h to 100 %") counted feature presence. This one
|
||||
measures readiness and adds hardware, localization and field bug reports.
|
||||
|
||||
An alpha is a build people can download, open the demo or their own audio in, record, edit, mix
|
||||
and bounce without crashes or lost work. Feature-wise SoundCraft is past that bar; what is left is
|
||||
shipping and hardening:
|
||||
## By dimension
|
||||
|
||||
1. **Publish the repository and cut the first release** (needs a decision from the maintainers:
|
||||
the GitHub repo, the release secrets, and a push to the `release` branch). The pipelines exist
|
||||
but have never run end to end. ~2 h once unblocked, mostly fixing what the first run finds.
|
||||
2. **Platform smoke tests** of the installers on Windows, Linux and FreeBSD (only macOS has been
|
||||
exercised by hand). ~3–4 h.
|
||||
3. **Disk streaming** of long sources, so an hour-long multitrack session does not need all of its
|
||||
audio in RAM. ~5–6 h.
|
||||
4. **A hands-on QA pass** in the live app: recording on real interfaces, long sessions, plugin-heavy
|
||||
mixes, save/reopen loops. ~3–4 h.
|
||||
5. **Docs**: a short user guide next to the existing agent/CLI docs. ~1 h.
|
||||
| Dimension | Ready | Remaining (h) | Doc |
|
||||
|---|---:|---:|---|
|
||||
| Features | ~45 % | 520–810 | [target-app-parity.md](docs/target-app-parity.md#feature-areas) |
|
||||
| UI/UX fidelity | ~55 % | 60–100 | [ui-parity.md](docs/ui-parity.md) |
|
||||
| File formats | ~40 % | 70–120 | [file-format-parity.md](docs/file-format-parity.md) |
|
||||
| Hardware (interfaces, MIDI, surfaces, sync) | ~12 % | 110–180 | [hardware-parity.md](docs/hardware-parity.md) |
|
||||
| Localization | ~12 % (1 of Pro Tools' 8 languages) | 70–110 | [localization-parity.md](docs/localization-parity.md) |
|
||||
| Performance | ~35 % (unmeasured) | 40–70 | [hardware-parity.md](docs/hardware-parity.md#performance) |
|
||||
| Stability | ~45 % | 50–80 | [gaps.md](docs/gaps.md) |
|
||||
| Platforms | ~65 % (5 platforms built, macOS exercised) | 20–40 | [gaps.md](docs/gaps.md) |
|
||||
| Ecosystem / plugins | ~30 % | in Features + Hardware | [plugin-parity.md](docs/plugin-parity.md) |
|
||||
| AI features | ~5 % | 30–50 | [gaps.md](docs/gaps.md) |
|
||||
| Agent control | ahead of Pro Tools | 3 | [control-protocol.md](docs/control-protocol.md), [mcp.md](docs/mcp.md) |
|
||||
|
||||
## Features
|
||||
|
||||
| Area | Breadth | Ready | Remaining (h) |
|
||||
|---|---:|---:|---:|
|
||||
| Recording and monitoring | 75 % | 40 % | 40–60 |
|
||||
| Editing | 92 % | 65 % | 40–60 |
|
||||
| Mixing and routing | 85 % | 60 % | 35–50 |
|
||||
| Automation | 80 % | 55 % | 20–30 |
|
||||
| Built-in plugins and AudioSuite | 40 % | 35 % | 80–120 |
|
||||
| Third-party plugin hosting | 70 % | 45 % | 40–60 |
|
||||
| MIDI and instruments | 70 % | 30 % | 50–80 |
|
||||
| Score and notation | 60 % | 35 % | 20–30 |
|
||||
| Elastic Audio, TCE, Beat Detective | 70 % | 35 % | 40–60 |
|
||||
| Surround and immersive | 55 % | 35 % | 50–80 |
|
||||
| Video and post | 60 % | 40 % | 40–60 |
|
||||
| Session, file management, interchange | 55 % | 35 % | 60–100 |
|
||||
| Collaboration and cloud (Avid services) | 0 % | 0 % | owner decision |
|
||||
| Agent control | 100 % | 100 % | 3 |
|
||||
|
||||
Weights and evidence per row: [target-app-parity.md](docs/target-app-parity.md#feature-areas).
|
||||
|
||||
## Languages
|
||||
|
||||
| Language | Code | Status | Translated |
|
||||
|---|---|---|---:|
|
||||
| English | en | full | 100 % |
|
||||
| Simplified Chinese | zh-Hans | none | 0 % |
|
||||
| Spanish | es | none (PR #143 open) | 0 % |
|
||||
| Hindi | hi | none | 0 % |
|
||||
| Arabic | ar | none | 0 % |
|
||||
| French | fr | none (PR #143 open) | 0 % |
|
||||
| Portuguese | pt | none | 0 % |
|
||||
| Indonesian | id | none | 0 % |
|
||||
| Japanese | ja | none | 0 % |
|
||||
| German | de | none | 0 % |
|
||||
| Korean | ko | none | 0 % |
|
||||
| Vietnamese | vi | none | 0 % |
|
||||
|
||||
Other languages shipped: 0. Detail: [localization-parity.md](docs/localization-parity.md).
|
||||
|
||||
## Upcoming
|
||||
|
||||
Ranked; the alpha gate, the alpha checklist and the beta plan are in [docs/roadmap.md](docs/roadmap.md).
|
||||
|
||||
1. **Alpha blocker:** audio interfaces: device and buffer-size choice, duplex stream, ASIO; record to disk (G-01, G-06). 35–55 h.
|
||||
2. **Alpha blocker:** MIDI devices: record from keyboards, play instruments live, output (G-02). 20–35 h.
|
||||
3. **Alpha blocker:** AAF/OMF import and export (G-03). 50–90 h.
|
||||
4. macOS plugin entitlement (#153), then AU editors (G-05). 1 h for the entitlement (needed for the mix row), then 15–25 h.
|
||||
5. Land the open bug-fix PRs and clear the correctness backlog (G-04). 15–25 h.
|
||||
6. PR CI running `cargo xtask ci` on three OSes (G-14). 5–10 h.
|
||||
7. Fidelity harness against Pro Tools (G-07). 20–30 h.
|
||||
|
||||
## Robustness
|
||||
|
||||
@@ -57,22 +117,34 @@ command parameters through the engine and mixer; they have caught fade, overflow
|
||||
unbounded-allocation bugs. Every command also runs with empty parameters on empty and demo
|
||||
sessions. Hosted plugins live in isolated unsafe crates (`clap-host`, `vst3-host`, `au-host`) and
|
||||
are created, loaded and destroyed off the audio thread. The video decoders survive hundreds of
|
||||
mutated and truncated movies.
|
||||
mutated and truncated movies. Field reports from the first public week are tracked in
|
||||
[gaps.md](docs/gaps.md) (G-04).
|
||||
|
||||
## Current focus
|
||||
## Progress log
|
||||
|
||||
1. The alpha list above.
|
||||
2. Surround-pan automation; AU editor views.
|
||||
3. Built-in plugin depth (more processors, better UIs).
|
||||
4. MIDI and score editing depth.
|
||||
- **2026-10-10** — Stage re-normalized to **pre-alpha** by the core-workflow gate (interface recording, MIDI recording and session interchange fail). Full re-measure against Pro Tools Ultimate 2026.4.1 and the standard
|
||||
progress-docs set (this page, target-app parity, gaps, roadmap, UI/format/hardware/plugin/
|
||||
localization parity). v0.4.0 released. Community fixes: Cut All Automation, snap to neighbours,
|
||||
tempo stretch, I/O Setup, clip export names, menu-only dialogs, playback stop on new session.
|
||||
- **2026-10-09** — Light theme and live system appearance, clip-gain fader and nudge, missing-media
|
||||
warnings, session audio health, malformed-JSON and encoder-extension checks in the CLI, macOS
|
||||
system menu bar, Windows DX12 default, Linux fontconfig, metronome click in playback.
|
||||
- **2026-10-08** — v0.2.x–v0.3.0 released (Flatpak, AppImage zsync); VST3 quit crash fixed;
|
||||
Ctrl-as-Cmd shortcuts on Windows/Linux; plugin menus by vendor, AU inserts, instrument picker;
|
||||
rotating log file; branded DMG.
|
||||
- **2026-10-07** — Repository published; About window credits; release CI on macOS runners.
|
||||
- **2026-10-06** — Video track, Audio Unit hosting, plugin state and editors, Renderer; Mix
|
||||
window views; score export and print; surround mixing; VST3 hosting; property tests.
|
||||
- **2026-10-05** — Foundation crates, Edit and Mix windows, playback, CLI/MCP, parity report,
|
||||
recording and punch, CLAP hosting, Elastic Audio, Beat Detective, release pipeline.
|
||||
|
||||
## Milestones
|
||||
## Revision history
|
||||
|
||||
- **M0–M4 (done):** workspace, gates, model, IO, DSP, mix engine, command engine, Edit/Mix UI, realtime playback, metering, plugins, bounce.
|
||||
- **M5 (done):** editing depth (tools, modes, playlists, fades, comping, clip groups).
|
||||
- **M6 (done for the first cut):** recording, punch, loop record, monitoring.
|
||||
- **M7 (in progress):** MIDI and notation editing.
|
||||
- **M8 (done):** CLI, control channel, MCP, agent acceptance test.
|
||||
- **M9 (in progress):** AudioSuite breadth (done), Elastic Audio, Beat Detective.
|
||||
- **M10 (next):** alpha release on all platforms.
|
||||
- **M11 (mostly done):** third-party plugin hosting (CLAP, VST3, AU), video, surround.
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-10 | minor | Readiness-by-audience table with hours: full 950–1,450 h, mainstream 380–600 h, essentials 110–200 h |
|
||||
| 2026-10-10 | minor | Added mainstream practitioner ~36 % and essentials user ~57 % to the headline numbers |
|
||||
| 2026-10-10 | minor | Stage alpha → pre-alpha: the core-workflow gate fails on interface recording, MIDI recording and session interchange; alpha distance added |
|
||||
| 2026-10-10 | major | Rewritten to the progress-docs standard: stage alpha (re-normalized the same day), two numbers (breadth ~70 %, ready ~40 %), dimension/feature/language tables, hours re-calibrated; area table moved to docs/target-app-parity.md, milestones to docs/roadmap.md |
|
||||
| 2026-10-07 | major | Menu parity 94 %, ~65 % estimated feature parity, alpha list |
|
||||
| 2026-10-06 | minor | Alpha and parity estimates |
|
||||
|
||||
@@ -351,7 +351,7 @@ fn mcp(args: &[String]) -> ExitCode {
|
||||
fn parity(args: &[String]) -> ExitCode {
|
||||
let p = soundcraft_engine::catalog::parity_json();
|
||||
let mut md = String::new();
|
||||
md.push_str("# Parity report\n\nGenerated by `soundcraft-cli parity --write docs/parity.md` (also `cargo xtask parity`).\n");
|
||||
md.push_str("# Parity report\n\nGenerated by `soundcraft-cli parity --write docs/parity-checklist.md` (also `cargo xtask parity`).\n");
|
||||
md.push_str("It compares the incumbent's menu tree (feature names observed black-box, `crates/engine/catalog/menus.txt`) with SoundCraft's command registry.\n");
|
||||
md.push_str("UI-layer commands (windows, AudioSuite) are counted by the app's own report (`engine.parity` over the control channel).\n\n");
|
||||
md.push_str(&format!(
|
||||
|
||||
@@ -2,36 +2,36 @@
|
||||
"schema": 1,
|
||||
"repo": "storytold/soundcraft",
|
||||
"branch": "main",
|
||||
"head": "caa4872df2aa81a21e401071d2e0d09002658e9e",
|
||||
"head_date": "2026-10-10T00:50:08Z",
|
||||
"commits": 98,
|
||||
"head": "330a6983973dc2043bb1fa0f0ba67fa07ae50685",
|
||||
"head_date": "2026-10-10T18:08:49Z",
|
||||
"commits": 111,
|
||||
"unattributed_commits": 0,
|
||||
"contributors": [
|
||||
{
|
||||
"login": "echelon",
|
||||
"people_entry": true,
|
||||
"github_name": null,
|
||||
"prs": 5,
|
||||
"commits": 56,
|
||||
"lines_added": 79132,
|
||||
"lines_deleted": 1983,
|
||||
"prs": 6,
|
||||
"commits": 58,
|
||||
"lines_added": 80505,
|
||||
"lines_deleted": 2222,
|
||||
"binary_added": 23,
|
||||
"binary_deleted": 0,
|
||||
"first_commit": "2026-10-06T00:08:04Z",
|
||||
"last_commit": "2026-10-10T00:50:08Z"
|
||||
"last_commit": "2026-10-10T18:08:49Z"
|
||||
},
|
||||
{
|
||||
"login": "reoniazx",
|
||||
"people_entry": false,
|
||||
"github_name": null,
|
||||
"prs": 3,
|
||||
"commits": 7,
|
||||
"lines_added": 453,
|
||||
"lines_deleted": 62,
|
||||
"prs": 5,
|
||||
"commits": 9,
|
||||
"lines_added": 649,
|
||||
"lines_deleted": 64,
|
||||
"binary_added": 0,
|
||||
"binary_deleted": 0,
|
||||
"first_commit": "2026-10-08T06:10:03Z",
|
||||
"last_commit": "2026-10-09T15:12:12Z"
|
||||
"last_commit": "2026-10-10T05:15:18Z"
|
||||
},
|
||||
{
|
||||
"login": "switch87",
|
||||
@@ -115,14 +115,14 @@
|
||||
"login": "vbasky",
|
||||
"people_entry": false,
|
||||
"github_name": null,
|
||||
"prs": 3,
|
||||
"commits": 4,
|
||||
"lines_added": 829,
|
||||
"lines_deleted": 137,
|
||||
"prs": 10,
|
||||
"commits": 11,
|
||||
"lines_added": 1039,
|
||||
"lines_deleted": 174,
|
||||
"binary_added": 0,
|
||||
"binary_deleted": 0,
|
||||
"first_commit": "2026-10-09T00:12:26Z",
|
||||
"last_commit": "2026-10-09T22:37:04Z"
|
||||
"last_commit": "2026-10-10T05:22:24Z"
|
||||
},
|
||||
{
|
||||
"login": "DWAA1660",
|
||||
@@ -167,14 +167,27 @@
|
||||
"login": "grovebit",
|
||||
"people_entry": false,
|
||||
"github_name": null,
|
||||
"prs": 1,
|
||||
"commits": 1,
|
||||
"lines_added": 198,
|
||||
"lines_deleted": 5,
|
||||
"prs": 2,
|
||||
"commits": 2,
|
||||
"lines_added": 218,
|
||||
"lines_deleted": 15,
|
||||
"binary_added": 0,
|
||||
"binary_deleted": 0,
|
||||
"first_commit": "2026-10-09T21:45:39Z",
|
||||
"last_commit": "2026-10-09T21:45:39Z"
|
||||
"last_commit": "2026-10-10T01:34:26Z"
|
||||
},
|
||||
{
|
||||
"login": "coygeek",
|
||||
"people_entry": false,
|
||||
"github_name": null,
|
||||
"prs": 1,
|
||||
"commits": 1,
|
||||
"lines_added": 125,
|
||||
"lines_deleted": 91,
|
||||
"binary_added": 0,
|
||||
"binary_deleted": 0,
|
||||
"first_commit": "2026-10-10T01:28:07Z",
|
||||
"last_commit": "2026-10-10T01:28:07Z"
|
||||
}
|
||||
],
|
||||
"models": [
|
||||
@@ -183,9 +196,9 @@
|
||||
"company": "Anthropic",
|
||||
"model": "Claude Opus",
|
||||
"version": "5.5",
|
||||
"commits": 71,
|
||||
"lines_added": 80906,
|
||||
"lines_deleted": 2305
|
||||
"commits": 73,
|
||||
"lines_added": 82279,
|
||||
"lines_deleted": 2544
|
||||
},
|
||||
{
|
||||
"id": "anthropic/claude-haiku/5.5",
|
||||
|
||||
@@ -71,8 +71,9 @@ unsafe fn read_cstr(p: *const c_char) -> Option<String> {
|
||||
}
|
||||
|
||||
/// Reads a fixed-size `char[N]` name buffer, stopping at the first NUL or the end of the array.
|
||||
/// `c_char` is `i8` on x86 and `u8` on ARM/RISC-V Linux; the byte reinterpretation is the same for both.
|
||||
fn read_name(buf: &[c_char]) -> String {
|
||||
let bytes: Vec<u8> = buf.iter().take_while(|c| **c != 0).map(|c| *c as u8).collect();
|
||||
let bytes: Vec<u8> = buf.iter().take_while(|c| **c != 0).map(|c| u8::from_ne_bytes(c.to_ne_bytes())).collect();
|
||||
String::from_utf8_lossy(&bytes).into_owned()
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,9 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
p,
|
||||
"edit.clear_clip_gain"
|
||||
)),
|
||||
cmd!("edit.cut_all_automation", "All Automation", ["Edit", "Cut Special"], None, "{}", has_range, |e, p| clear_automation(e, p, None)),
|
||||
cmd!("edit.cut_all_automation", "All Automation", ["Edit", "Cut Special"], None, "{tracks?, start?, end?}", has_range, |e, p| {
|
||||
super::edit_more::cut_all_automation(e, p)
|
||||
}),
|
||||
cmd!("edit.clear_all_automation", "All Automation", ["Edit", "Clear Special"], None, "{}", has_range, |e, p| clear_automation(e, p, None)),
|
||||
cmd!("edit.clear_pan_automation", "Pan Automation", ["Edit", "Clear Special"], None, "{}", has_range, |e, p| clear_automation(
|
||||
e,
|
||||
|
||||
@@ -277,6 +277,10 @@ fn nonempty_range(e: &Engine, p: &Value, id: &str) -> Result<Range> {
|
||||
if r.is_empty() { Err(bad(id, "the selection is empty")) } else { Ok(r) }
|
||||
}
|
||||
|
||||
pub(super) fn cut_all_automation(e: &mut Engine, p: &Value) -> Result<Value> {
|
||||
auto_copy(e, p, Lanes::All, true, "edit.cut_all_automation")
|
||||
}
|
||||
|
||||
fn auto_copy(e: &mut Engine, p: &Value, which: Lanes, cut: bool, id: &str) -> Result<Value> {
|
||||
let tracks = tracks_required(e, id, p)?;
|
||||
let r = nonempty_range(e, p, id)?;
|
||||
@@ -663,8 +667,10 @@ fn snap(e: &mut Engine, p: &Value, next: bool) -> Result<Value> {
|
||||
let mut moved = 0;
|
||||
for (id, _) in order {
|
||||
let Some((t, c)) = s.find_clip(id).map(|(t, c)| (t, c.clone())) else { continue };
|
||||
let others =
|
||||
s.track(t).map(|tr| tr.clips().iter().filter(|x| x.id != id && !ids.contains(&x.id)).cloned().collect::<Vec<_>>()).unwrap_or_default();
|
||||
// Selected neighbours are real destinations. Ignoring them made Snap to Next
|
||||
// pile every selected clip onto the same later clip, and Snap to Previous
|
||||
// leave the gap between the selected clips in place.
|
||||
let others = s.track(t).map(|tr| tr.clips().iter().filter(|x| x.id != id).cloned().collect::<Vec<_>>()).unwrap_or_default();
|
||||
let delta = if next {
|
||||
others.iter().filter(|x| x.start >= c.end()).map(|x| x.start).min().map(|st| st - c.end())
|
||||
} else {
|
||||
@@ -982,6 +988,34 @@ mod tests {
|
||||
assert_eq!(starts(&e, t), vec![0, 900, 1000]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cut_all_automation_can_be_pasted() {
|
||||
let (mut e, t, _) = session_with_clips(&[(0, 1000)]);
|
||||
e.execute("automation.set_point", &json!({"track": t.0, "param": "volume", "at": 100, "value": -6.0})).unwrap();
|
||||
e.execute("automation.set_point", &json!({"track": t.0, "param": "volume", "at": 400, "value": -12.0})).unwrap();
|
||||
let cut = e.execute("edit.cut_all_automation", &json!({"start": 50, "end": 250})).unwrap();
|
||||
assert!(cut["lanes"].as_u64().unwrap() >= 1, "{cut}");
|
||||
assert!(e.clipboard.automation.iter().any(|lanes| !lanes.is_empty()));
|
||||
let lane = e.session().track(t).unwrap().lane(&AutoParam::Volume).unwrap().clone();
|
||||
assert!(lane.points.iter().all(|p| p.at != 100), "the cut point is gone: {:?}", lane.points);
|
||||
e.execute("edit.paste_to_current_automation", &json!({"at": 1000})).unwrap();
|
||||
let vol = e.session().track(t).unwrap().lane(&AutoParam::Volume).unwrap().clone();
|
||||
assert!(vol.points.iter().any(|p| p.at >= 1000 && (p.value + 6.0).abs() < 1e-2), "paste wrote nothing at -6 dB: {:?}", vol.points);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snap_selected_neighbours_keeps_both_clips() {
|
||||
let (mut e, t, ids) = session_with_clips(&[(0, 100), (300, 100), (1000, 100)]);
|
||||
e.execute("edit.snap_next", &json!({"clips": [ids[0].0, ids[1].0]})).unwrap();
|
||||
assert_eq!(e.session().track(t).unwrap().clips().len(), 3);
|
||||
assert_eq!(starts(&e, t), vec![800, 900, 1000]);
|
||||
|
||||
let (mut e, t, ids) = session_with_clips(&[(0, 100), (300, 100), (1000, 100)]);
|
||||
e.execute("edit.snap_previous", &json!({"clips": [ids[0].0, ids[1].0]})).unwrap();
|
||||
assert_eq!(e.session().track(t).unwrap().clips().len(), 3);
|
||||
assert_eq!(starts(&e, t), vec![0, 100, 1000]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn copy_and_paste_automation_only() {
|
||||
let (mut e, t, _) = session_with_clips(&[(0, 1000)]);
|
||||
|
||||
@@ -390,4 +390,42 @@ mod tests {
|
||||
assert!(res["missing"].as_array().is_some_and(|m| !m.is_empty()), "{res}");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
fn tone_engine() -> (Engine, u64) {
|
||||
let mut e = Engine::default();
|
||||
e.execute("track.new", &json!({"count": 1, "name": "Tone", "format": "mono"})).unwrap();
|
||||
let track = e.session().tracks[0].id;
|
||||
let tone: Vec<f32> = (0..4_800).map(|i| (i as f32 * 0.05).sin() * 0.5).collect();
|
||||
let buf = AudioBuffer { sample_rate: 48_000, channels: vec![tone] };
|
||||
let src = crate::io::add_source(e.session_mut(), "tone", buf, None, soundcraft_audio_io::FileFormat::Wav);
|
||||
let id = e.session_mut().new_clip_id();
|
||||
crate::edit::place_clip(e.session_mut(), track, soundcraft_model::Clip::audio(id, "tone", src, 0, 0, 4_800));
|
||||
e.session_mut().edit.selected_tracks = vec![track];
|
||||
(e, track.0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_named_clips_export_as_separate_files() {
|
||||
let (mut e, track) = tone_engine();
|
||||
let first = e.session().tracks[0].clips()[0].id;
|
||||
let src = e.session().tracks[0].clips()[0].source().unwrap();
|
||||
let second = e.session_mut().new_clip_id();
|
||||
crate::edit::place_clip(
|
||||
e.session_mut(),
|
||||
soundcraft_model::TrackId(track),
|
||||
soundcraft_model::Clip::audio(second, "tone", src, 0, 10_000, 4_800),
|
||||
);
|
||||
for id in [first, second] {
|
||||
e.session_mut().find_clip_mut(id).unwrap().name = "Take".into();
|
||||
}
|
||||
let dir = std::env::temp_dir().join(format!("sc-export-clips-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let n = e.execute("file.export_clips", &json!({"dir": dir.to_string_lossy(), "clips": [first.0, second.0]})).unwrap();
|
||||
assert_eq!(n["written"], 2);
|
||||
let mut wavs: Vec<_> = std::fs::read_dir(&dir).unwrap().flatten().map(|e| e.file_name().to_string_lossy().into_owned()).collect();
|
||||
wavs.sort();
|
||||
assert_eq!(wavs, vec!["Take-2.wav".to_string(), "Take.wav".to_string()]);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
}
|
||||
|
||||
+10
-1
@@ -432,7 +432,16 @@ pub fn export_clips(e: &Engine, ids: &[soundcraft_model::ClipId], dir: &str) ->
|
||||
let audio = soundcraft_mix::render_clips(s, t, c.range());
|
||||
let buf = AudioBuffer { sample_rate: s.sample_rate.hz(), channels: audio };
|
||||
let bytes = soundcraft_audio_io::encode(&buf, &EncodeOptions::default()).map_err(|err| EngineError::Io(err.to_string()))?;
|
||||
let path = Path::new(dir).join(format!("{}.wav", sanitize_name(&c.name)));
|
||||
let stem = sanitize_name(&c.name);
|
||||
let mut path = Path::new(dir).join(format!("{stem}.wav"));
|
||||
let mut k = 2u32;
|
||||
while path.exists() {
|
||||
path = Path::new(dir).join(format!("{stem}-{k}.wav"));
|
||||
k = k.saturating_add(1);
|
||||
if k > 10_000 {
|
||||
return Err(EngineError::Io(format!("{dir}: too many files named {stem}")));
|
||||
}
|
||||
}
|
||||
std::fs::write(&path, bytes).map_err(|err| EngineError::Io(format!("{}: {err}", path.display())))?;
|
||||
n += 1;
|
||||
}
|
||||
|
||||
@@ -53,6 +53,8 @@ pub type Result<T> = std::result::Result<T, EngineError>;
|
||||
/// Requests for the transport/audio engine, drained by the host app each frame.
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize)]
|
||||
pub enum TransportRequest {
|
||||
/// Stop the old session's transport without applying stop-time edits to the new document.
|
||||
ResetSession,
|
||||
Play,
|
||||
Stop,
|
||||
TogglePlay,
|
||||
@@ -158,6 +160,9 @@ impl Engine {
|
||||
|
||||
/// Replace the whole document (open / new); clears history.
|
||||
pub fn replace_session(&mut self, s: Session) {
|
||||
self.transport = TransportStatus { position: s.edit.selection.start.max(0), ..TransportStatus::default() };
|
||||
self.transport_requests.clear();
|
||||
self.transport_requests.push(TransportRequest::ResetSession);
|
||||
self.doc = Arc::new(s);
|
||||
self.undo.clear();
|
||||
self.redo.clear();
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
use serde_json::json;
|
||||
use soundcraft_engine::{Engine, TransportRequest, TransportStatus};
|
||||
use soundcraft_model::Session;
|
||||
use soundcraft_time::Range;
|
||||
|
||||
#[test]
|
||||
fn replacing_session_resets_status_and_discards_old_transport_requests() {
|
||||
let mut engine = Engine::default();
|
||||
engine.transport = TransportStatus { playing: true, recording: true, position: 48_000 };
|
||||
engine.execute("transport.play", &json!({"from": 96_000})).unwrap();
|
||||
engine.execute("transport.record", &json!({})).unwrap();
|
||||
let mut session = Session::default();
|
||||
session.edit.selection = Range::point(12_000);
|
||||
engine.replace_session(session);
|
||||
assert_eq!(engine.transport, TransportStatus { position: 12_000, ..TransportStatus::default() });
|
||||
assert_eq!(engine.transport_requests, vec![TransportRequest::ResetSession]);
|
||||
assert!(!engine.is_dirty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_new_session_keeps_the_current_transport() {
|
||||
let mut engine = Engine::default();
|
||||
engine.transport = TransportStatus { playing: true, position: 48_000, ..TransportStatus::default() };
|
||||
engine.execute("transport.play", &json!({})).unwrap();
|
||||
let status = engine.transport;
|
||||
let requests = engine.transport_requests.clone();
|
||||
assert!(engine.execute("session.new", &json!({"sample_rate": 0})).is_err());
|
||||
assert_eq!(engine.transport, status);
|
||||
assert_eq!(engine.transport_requests, requests);
|
||||
}
|
||||
@@ -21,7 +21,6 @@ soundcraft-engine.workspace = true
|
||||
soundcraft-playback.workspace = true
|
||||
soundcraft-video.workspace = true
|
||||
egui.workspace = true
|
||||
dark-light.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
log.workspace = true
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
//! Render the real UI offscreen (no window, no focus stealing) and save PNGs.
|
||||
//!
|
||||
//! `cargo run -p soundcraft-ui-egui --example ui_shot -- out.png [script.jsonl] [--blank] [--size WxH]`
|
||||
//! `cargo run -p soundcraft-ui-egui --example ui_shot -- out.png [script.jsonl] [--blank] [--size WxH] [--system-theme light|dark|none]`
|
||||
//!
|
||||
//! Script lines are control requests (`{"method": "...", "params": {...}}`), `{"shot": "path.png"}`
|
||||
//! or `{"steps": n}`. The demo session is loaded unless `--blank`.
|
||||
//! or `{"steps": n}`. `{"system_theme": "light"}` (also `"dark"` or `"none"`) changes the
|
||||
//! simulated OS appearance for subsequent frames. The demo session is loaded unless `--blank`.
|
||||
|
||||
use soundcraft_ui_egui::{ControlRequest, Services, SoundApp};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
@@ -11,11 +12,29 @@ use std::sync::mpsc;
|
||||
|
||||
static READY: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
fn system_theme(value: &str) -> Option<Option<egui::Theme>> {
|
||||
match value {
|
||||
"light" => Some(Some(egui::Theme::Light)),
|
||||
"dark" => Some(Some(egui::Theme::Dark)),
|
||||
"none" => Some(None),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||
let out = args.first().cloned().unwrap_or_else(|| "soundcraft.png".into());
|
||||
let script = args.get(1).filter(|a| !a.starts_with("--")).cloned();
|
||||
let blank = args.iter().any(|a| a == "--blank");
|
||||
let appearance = if let Some(i) = args.iter().position(|a| a == "--system-theme") {
|
||||
let Some(appearance) = args.get(i + 1).and_then(|value| system_theme(value)) else {
|
||||
eprintln!("--system-theme requires light, dark or none");
|
||||
return;
|
||||
};
|
||||
appearance
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let (w, h) = args
|
||||
.iter()
|
||||
.position(|a| a == "--size")
|
||||
@@ -39,6 +58,7 @@ fn main() {
|
||||
app,
|
||||
);
|
||||
harness.input_mut().max_texture_side = Some(8192);
|
||||
harness.input_mut().system_theme = appearance;
|
||||
READY.store(true, Ordering::Relaxed);
|
||||
let step = |h: &mut egui_kittest::Harness<SoundApp>| {
|
||||
let mut raw = std::mem::take(h.input_mut());
|
||||
@@ -63,6 +83,15 @@ fn main() {
|
||||
eprintln!("bad line: {line}");
|
||||
continue;
|
||||
};
|
||||
if let Some(value) = v.get("system_theme") {
|
||||
let Some(appearance) = value.as_str().and_then(system_theme) else {
|
||||
eprintln!("system_theme requires light, dark or none: {line}");
|
||||
continue;
|
||||
};
|
||||
harness.input_mut().system_theme = appearance;
|
||||
step(&mut harness);
|
||||
continue;
|
||||
}
|
||||
if let Some(p) = v.get("shot").and_then(|x| x.as_str()) {
|
||||
for _ in 0..3 {
|
||||
step(&mut harness);
|
||||
|
||||
@@ -642,6 +642,25 @@ impl SoundApp {
|
||||
let reqs: Vec<TransportRequest> = std::mem::take(&mut self.engine.transport_requests);
|
||||
for r in reqs {
|
||||
match r {
|
||||
TransportRequest::ResetSession => {
|
||||
if let Some(p) = &self.player {
|
||||
p.stop();
|
||||
p.set_recording(false);
|
||||
p.set_speed(1.0);
|
||||
p.locate(self.engine.transport.position);
|
||||
}
|
||||
self.sim = None;
|
||||
// The old capture belongs to the replaced document, never to the new one.
|
||||
self.recorder = None;
|
||||
self.recorder_failed = false;
|
||||
self.punch = None;
|
||||
self.gesture = None;
|
||||
self.touched.clear();
|
||||
self.last_write_at = i64::MIN;
|
||||
self.edit_layout.follow_hold = false;
|
||||
self.meters.clear();
|
||||
self.main_meter = MeterDisplay::default();
|
||||
}
|
||||
TransportRequest::Play => {
|
||||
if !self.is_playing() {
|
||||
self.play_from_selection();
|
||||
@@ -761,13 +780,14 @@ impl SoundApp {
|
||||
self.engine.end_merge();
|
||||
}
|
||||
}
|
||||
// Stop the old playback before sending a replacement session to the audio thread.
|
||||
self.handle_transport(dt);
|
||||
if self.engine.revision != self.last_rev {
|
||||
self.last_rev = self.engine.revision;
|
||||
if let Some(p) = &self.player {
|
||||
p.update_session(self.engine.session_arc());
|
||||
}
|
||||
}
|
||||
self.handle_transport(dt);
|
||||
self.write_automation(ctx);
|
||||
self.autosave(now);
|
||||
if self.recorder.is_none()
|
||||
@@ -818,6 +838,8 @@ impl SoundApp {
|
||||
/// Lay out the whole window.
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
let ctx = ui.ctx().clone();
|
||||
// The integration creates this Ui before logic resolves the current frame's palette.
|
||||
ui.set_style(ctx.global_style());
|
||||
if !self.fonts_ready {
|
||||
ctx.request_repaint();
|
||||
return;
|
||||
@@ -889,7 +911,64 @@ fn feed_meter(d: &mut MeterDisplay, peak: [f32; 2], gr: f32, dt: f32) {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::automation_step_due;
|
||||
use super::{Services, SoundApp, automation_step_due};
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn new_session_stops_playback_and_the_old_clock() {
|
||||
for (command, params) in [
|
||||
("session.new", json!({"name": "New project", "sample_rate": 96_000})),
|
||||
("session.new", json!({"template": "demo"})),
|
||||
("session.close", json!({})),
|
||||
] {
|
||||
let mut app = SoundApp::new(soundcraft_engine::demo::demo_engine(), None, Services::default());
|
||||
app.run("transport.play", json!({"from": 48_000})).unwrap();
|
||||
app.handle_transport(0.25);
|
||||
assert!(app.is_playing());
|
||||
assert!(app.position() > 48_000);
|
||||
app.run(command, params).unwrap();
|
||||
let insertion = app.engine.session().edit.selection.start;
|
||||
app.handle_transport(0.25);
|
||||
assert!(!app.is_playing(), "{command}");
|
||||
assert_eq!(app.position(), insertion, "{command}");
|
||||
assert_eq!(app.engine.transport.position, insertion, "{command}");
|
||||
app.handle_transport(0.25);
|
||||
assert_eq!(app.engine.transport.position, insertion, "the old clock must stay stopped");
|
||||
app.run("transport.play", json!({})).unwrap();
|
||||
app.handle_transport(0.25);
|
||||
assert!(app.is_playing());
|
||||
assert_eq!(app.position(), insertion + i64::from(app.engine.session().sample_rate.hz()) / 4);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_session_cancels_play_queued_before_the_next_frame() {
|
||||
let mut app = SoundApp::new(soundcraft_engine::demo::demo_engine(), None, Services::default());
|
||||
app.run("transport.play", json!({})).unwrap();
|
||||
app.run("session.new", json!({})).unwrap();
|
||||
app.handle_transport(0.25);
|
||||
assert!(!app.is_playing());
|
||||
assert_eq!(app.engine.transport.position, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_reset_does_not_apply_old_stop_edits_to_the_new_session() {
|
||||
let mut app = SoundApp::new(soundcraft_engine::demo::demo_engine(), None, Services::default());
|
||||
app.run("transport.play", json!({"from": 48_000})).unwrap();
|
||||
app.handle_transport(0.25);
|
||||
let mut session = soundcraft_model::Session::default();
|
||||
session.edit.selection = soundcraft_time::Range::new(12_000, 24_000);
|
||||
session.edit.insertion_follows_playback = true;
|
||||
session.edit.scrolling = "after_playback".into();
|
||||
let selection = session.edit.selection;
|
||||
app.engine.replace_session(session);
|
||||
app.handle_transport(0.25);
|
||||
assert!(!app.is_playing());
|
||||
assert_eq!(app.engine.session().edit.selection, selection);
|
||||
assert_eq!(app.position(), selection.start);
|
||||
assert_eq!(app.engine.session().edit.zoom.scroll, 0);
|
||||
assert!(!app.engine.is_dirty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn automation_first_pass_after_start_is_due_without_overflow() {
|
||||
|
||||
+73
-25
@@ -74,29 +74,41 @@ pub const UI_COMMANDS: &[(&str, &str, &str, Option<&str>)] = &[
|
||||
/// Extra catalog mappings handled by the UI layer.
|
||||
pub fn ui_aliases() -> Vec<(&'static str, &'static str)> {
|
||||
let mut v: Vec<(&str, &str)> = UI_COMMANDS.iter().filter(|c| !c.2.is_empty()).map(|c| (c.2, c.0)).collect();
|
||||
for sec in [
|
||||
"Inserts A-E",
|
||||
"Inserts F-J",
|
||||
"Sends A-E",
|
||||
"Sends F-J",
|
||||
"I/O",
|
||||
"Track Color",
|
||||
"Comments",
|
||||
"EQ Curve",
|
||||
"Meters and Faders",
|
||||
"All",
|
||||
"Minimal",
|
||||
"Mic Preamps",
|
||||
"Instruments",
|
||||
"Object",
|
||||
] {
|
||||
v.push((Box::leak(format!("View > Mix Window Views > {sec}").into_boxed_str()), "view.mix_section"));
|
||||
}
|
||||
v.extend(mix_view_aliases().iter().copied());
|
||||
v.extend(AUDIOSUITE.iter().map(|(p, _)| (*p, "audiosuite.process")));
|
||||
v.extend(MENU_WINDOWS.iter().copied());
|
||||
v
|
||||
}
|
||||
|
||||
/// Mix-view menu paths, allocated once. Rebuilding them on every menu draw leaked a copy per frame.
|
||||
fn mix_view_aliases() -> &'static [(&'static str, &'static str)] {
|
||||
static ALIASES: std::sync::OnceLock<Vec<(&'static str, &'static str)>> = std::sync::OnceLock::new();
|
||||
ALIASES.get_or_init(|| {
|
||||
[
|
||||
"Inserts A-E",
|
||||
"Inserts F-J",
|
||||
"Sends A-E",
|
||||
"Sends F-J",
|
||||
"I/O",
|
||||
"Track Color",
|
||||
"Comments",
|
||||
"EQ Curve",
|
||||
"Meters and Faders",
|
||||
"All",
|
||||
"Minimal",
|
||||
"Mic Preamps",
|
||||
"Instruments",
|
||||
"Object",
|
||||
]
|
||||
.into_iter()
|
||||
.map(|sec| {
|
||||
let path: &'static str = Box::leak(format!("View > Mix Window Views > {sec}").into_boxed_str());
|
||||
(path, "view.mix_section")
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
/// AudioSuite catalog entries → our processes (functional equivalents, our own plugins).
|
||||
pub const AUDIOSUITE: &[(&str, &str)] = &[
|
||||
("AudioSuite > EQ > Channel Strip", "channel_strip"),
|
||||
@@ -390,6 +402,15 @@ pub fn invoke_menu(app: &mut SoundApp, id: &str, path: &str) {
|
||||
if wants_dialog(id) && app.dialogs.open_for_command(&app.engine, id) {
|
||||
return;
|
||||
}
|
||||
// These ids only exist to open a dialog from the menu. Programmatic `app.run` refuses them.
|
||||
if id == "ui.new_tracks_dialog" {
|
||||
let _ = app.dialogs.open_for_command(&app.engine, "track.new");
|
||||
return;
|
||||
}
|
||||
if id == "ui.bounce_dialog" {
|
||||
let _ = app.dialogs.open_for_command(&app.engine, "file.bounce_mix");
|
||||
return;
|
||||
}
|
||||
let params = params_for(path, id);
|
||||
let _ = app.run(id, params);
|
||||
}
|
||||
@@ -524,12 +545,10 @@ pub fn run_ui_command(app: &mut SoundApp, id: &str, p: &Value) -> Option<Result<
|
||||
json!({"mix_views": app.ui.mix_views})
|
||||
}
|
||||
"ui.new_tracks_dialog" => {
|
||||
let _ = app.dialogs.open_for_command(&app.engine, "track.new");
|
||||
json!({})
|
||||
return Some(Err("ui.new_tracks_dialog opens a dialog from the menu only; call track.new with parameters".into()));
|
||||
}
|
||||
"ui.bounce_dialog" => {
|
||||
let _ = app.dialogs.open_for_command(&app.engine, "file.bounce_mix");
|
||||
json!({})
|
||||
return Some(Err("ui.bounce_dialog opens a dialog from the menu only; call file.bounce_mix with a path".into()));
|
||||
}
|
||||
"ui.theme" => {
|
||||
let Some(mode) = p.get("mode").and_then(Value::as_str) else {
|
||||
@@ -538,9 +557,6 @@ pub fn run_ui_command(app: &mut SoundApp, id: &str, p: &Value) -> Option<Result<
|
||||
let Some(mode) = crate::theme::ThemeMode::parse(mode) else {
|
||||
return Some(Err(format!("unknown theme `{mode}` (system, light or dark)")));
|
||||
};
|
||||
if mode == crate::theme::ThemeMode::System {
|
||||
crate::theme::refresh_system();
|
||||
}
|
||||
app.ui.theme = mode;
|
||||
json!({"mode": mode.id()})
|
||||
}
|
||||
@@ -571,3 +587,35 @@ pub fn parity() -> Value {
|
||||
let extra = ui_aliases();
|
||||
catalog::parity_with(&extra)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{Services, SoundApp};
|
||||
|
||||
#[test]
|
||||
fn mix_view_aliases_are_allocated_once() {
|
||||
let first = ui_aliases();
|
||||
let second = ui_aliases();
|
||||
let a = first.iter().find(|(path, _)| path.contains("Mix Window Views > All")).map(|(path, _)| *path).unwrap();
|
||||
let b = second.iter().find(|(path, _)| path.contains("Mix Window Views > All")).map(|(path, _)| *path).unwrap();
|
||||
assert!(std::ptr::eq(a, b));
|
||||
assert_eq!(first.iter().filter(|(path, _)| path.contains("Mix Window Views")).count(), 14);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn programmatic_dialog_commands_stay_closed_and_the_menu_opens_them() {
|
||||
let mut app = SoundApp::new(soundcraft_engine::Engine::default(), None, Services::default());
|
||||
let err = app.run("ui.new_tracks_dialog", json!({})).unwrap_err();
|
||||
assert!(err.contains("track.new"), "{err}");
|
||||
let err = app.run("ui.bounce_dialog", json!({})).unwrap_err();
|
||||
assert!(err.contains("file.bounce_mix"), "{err}");
|
||||
assert!(app.dialogs.open_name().is_none());
|
||||
|
||||
invoke_menu(&mut app, "ui.new_tracks_dialog", "Track > New...");
|
||||
assert_eq!(app.dialogs.open_name(), Some("new_tracks"));
|
||||
app.dialogs.open = None;
|
||||
invoke_menu(&mut app, "ui.bounce_dialog", "File > Bounce Mix...");
|
||||
assert_eq!(app.dialogs.open_name(), Some("bounce"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +56,9 @@ fn is_black(p: i32) -> bool {
|
||||
}
|
||||
|
||||
pub fn show(app: &mut SoundApp, ui: &mut Ui) {
|
||||
// Painter-only contents do not advance the layout. Claim the resized
|
||||
// space, including the empty state, so the dock keeps its chosen height.
|
||||
ui.take_available_space();
|
||||
let t = Tokens::current();
|
||||
let full = ui.max_rect();
|
||||
ui.painter().rect_filled(full, 0.0, t.panel_bg);
|
||||
|
||||
@@ -123,6 +123,21 @@ fn beat_detective(app: &mut SoundApp, ctx: &egui::Context) {
|
||||
app.ui.show_beat_detective = open;
|
||||
}
|
||||
|
||||
/// Command the Tempo Operations Apply button sends. Stretch uses `factor = start / end`
|
||||
/// so a lower end BPM lengthens the selection (`event.tempo_stretch` has no `end_bpm`).
|
||||
pub(crate) fn tempo_ops_apply(curve: &str, start_bpm: f64, end_bpm: f64) -> (&'static str, Value) {
|
||||
let start = if start_bpm.is_finite() { start_bpm.max(1.0) } else { 1.0 };
|
||||
let end = if end_bpm.is_finite() { end_bpm.max(1.0) } else { 1.0 };
|
||||
match curve {
|
||||
"constant" => ("event.tempo_constant", json!({"bpm": start_bpm})),
|
||||
"parabolic" => ("event.tempo_parabolic", json!({"start_bpm": start_bpm, "end_bpm": end_bpm})),
|
||||
"s-curve" => ("event.tempo_s_curve", json!({"start_bpm": start_bpm, "end_bpm": end_bpm})),
|
||||
"scale" => ("event.tempo_scale", json!({"factor": end / start})),
|
||||
"stretch" => ("event.tempo_stretch", json!({"factor": start / end})),
|
||||
_ => ("event.tempo_linear", json!({"start_bpm": start_bpm, "end_bpm": end_bpm})),
|
||||
}
|
||||
}
|
||||
|
||||
fn tempo_ops(app: &mut SoundApp, ctx: &egui::Context) {
|
||||
let mut open = app.ui.show_tempo_ops;
|
||||
if !open {
|
||||
@@ -136,16 +151,14 @@ fn tempo_ops(app: &mut SoundApp, ctx: &egui::Context) {
|
||||
});
|
||||
ui.add(egui::DragValue::new(&mut app.ops.tempo_start).range(5.0..=999.0).prefix("start bpm "));
|
||||
ui.add(egui::DragValue::new(&mut app.ops.tempo_end).range(5.0..=999.0).prefix("end bpm "));
|
||||
ui.label(egui::RichText::new("Applies across the edit selection (constant: at the insertion point; scale: factor = end / start).").small());
|
||||
ui.label(
|
||||
egui::RichText::new(
|
||||
"Applies across the edit selection (constant: at the insertion point; scale: factor = end / start; stretch: factor = start / end).",
|
||||
)
|
||||
.small(),
|
||||
);
|
||||
if ui.button(egui::RichText::new("Apply").strong()).clicked() {
|
||||
let (id, p) = match app.ops.tempo_curve.as_str() {
|
||||
"constant" => ("event.tempo_constant", json!({"bpm": app.ops.tempo_start})),
|
||||
"parabolic" => ("event.tempo_parabolic", json!({"start_bpm": app.ops.tempo_start, "end_bpm": app.ops.tempo_end})),
|
||||
"s-curve" => ("event.tempo_s_curve", json!({"start_bpm": app.ops.tempo_start, "end_bpm": app.ops.tempo_end})),
|
||||
"scale" => ("event.tempo_scale", json!({"factor": app.ops.tempo_end / app.ops.tempo_start.max(1.0)})),
|
||||
"stretch" => ("event.tempo_stretch", json!({"end_bpm": app.ops.tempo_end})),
|
||||
_ => ("event.tempo_linear", json!({"start_bpm": app.ops.tempo_start, "end_bpm": app.ops.tempo_end})),
|
||||
};
|
||||
let (id, p) = tempo_ops_apply(&app.ops.tempo_curve, app.ops.tempo_start, app.ops.tempo_end);
|
||||
let r = app.run(id, p);
|
||||
result_line(ui, &r);
|
||||
}
|
||||
@@ -274,3 +287,25 @@ fn rtp(app: &mut SoundApp, ctx: &egui::Context) {
|
||||
});
|
||||
app.ui.show_rtp = open;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn stretch_uses_start_over_end_and_scale_uses_end_over_start() {
|
||||
let (id, p) = tempo_ops_apply("stretch", 120.0, 60.0);
|
||||
assert_eq!(id, "event.tempo_stretch");
|
||||
assert!((p["factor"].as_f64().unwrap() - 2.0).abs() < 1e-9, "{p}");
|
||||
let (_, p) = tempo_ops_apply("stretch", 120.0, 120.0);
|
||||
assert!((p["factor"].as_f64().unwrap() - 1.0).abs() < 1e-9, "{p}");
|
||||
let (id, p) = tempo_ops_apply("scale", 120.0, 60.0);
|
||||
assert_eq!(id, "event.tempo_scale");
|
||||
assert!((p["factor"].as_f64().unwrap() - 0.5).abs() < 1e-9, "{p}");
|
||||
let (_, p) = tempo_ops_apply("stretch", f64::NAN, f64::INFINITY);
|
||||
assert!((p["factor"].as_f64().unwrap() - 1.0).abs() < 1e-9, "{p}");
|
||||
let (id, p) = tempo_ops_apply("linear", 100.0, 140.0);
|
||||
assert_eq!(id, "event.tempo_linear");
|
||||
assert_eq!(p["end_bpm"], 140.0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,6 +170,12 @@ fn fixed(app: &mut SoundApp, key: Key, m: Modifiers, mac: bool) -> bool {
|
||||
let _ = app.run(id, json!({}));
|
||||
true
|
||||
}
|
||||
// Ctrl+Shift+Up/Down nudge clip gain off macOS too (the registry's Ctrl is the Mac Control key).
|
||||
Key::ArrowUp | Key::ArrowDown if !mac && m.ctrl && m.shift && !m.alt => {
|
||||
let id = if key == Key::ArrowUp { "clip.gain_nudge_up" } else { "clip.gain_nudge_down" };
|
||||
let _ = app.run(id, json!({}));
|
||||
true
|
||||
}
|
||||
// Commands Keyboard Focus: single-key editing (Edit window only).
|
||||
k if plain && app.ui.window == MainWindow::Edit && app.engine.session().edit.keyboard_focus == "commands" && commands_focus(app, k) => true,
|
||||
Key::R | Key::T if plain => {
|
||||
|
||||
+84
-46
@@ -1,18 +1,11 @@
|
||||
//! Design tokens. Colours were chosen by eye to give a dark, studio-style look; they are ours.
|
||||
|
||||
use egui::{Color32, FontFamily, FontId};
|
||||
use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
/// Whether the Light palette is active. Dark is the default (the incumbent is dark-only).
|
||||
static LIGHT_THEME: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
/// The operating system's appearance, asked once off the UI thread.
|
||||
static SYSTEM: AtomicU8 = AtomicU8::new(SYSTEM_UNKNOWN);
|
||||
const SYSTEM_UNKNOWN: u8 = 0;
|
||||
const SYSTEM_ASKING: u8 = 1;
|
||||
const SYSTEM_DARK: u8 = 2;
|
||||
const SYSTEM_LIGHT: u8 = 3;
|
||||
|
||||
/// The Appearance setting (`ui.theme {mode}`). Dark by default; System and Light are opt-in.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
@@ -335,49 +328,15 @@ pub fn is_light() -> bool {
|
||||
LIGHT_THEME.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Whether `mode` resolves to the Light palette right now. Never blocks.
|
||||
/// Resolve each frame from the integration's current OS appearance. Unknown uses Dark.
|
||||
pub fn wants_light(ctx: &egui::Context, mode: ThemeMode) -> bool {
|
||||
match mode {
|
||||
ThemeMode::Light => true,
|
||||
ThemeMode::Dark => false,
|
||||
ThemeMode::System => system_is_light(ctx),
|
||||
ThemeMode::System => ctx.system_theme() == Some(egui::Theme::Light),
|
||||
}
|
||||
}
|
||||
|
||||
/// Forget the cached system appearance so the next System lookup asks again.
|
||||
pub fn refresh_system() {
|
||||
let _ = SYSTEM.compare_exchange(SYSTEM_DARK, SYSTEM_UNKNOWN, Ordering::Relaxed, Ordering::Relaxed);
|
||||
let _ = SYSTEM.compare_exchange(SYSTEM_LIGHT, SYSTEM_UNKNOWN, Ordering::Relaxed, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// The OS appearance. The query (a D-Bus call on Linux) runs once on a background thread; until it
|
||||
/// answers this reports dark, and `ctx` is repainted when the answer arrives.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn system_is_light(ctx: &egui::Context) -> bool {
|
||||
if SYSTEM.compare_exchange(SYSTEM_UNKNOWN, SYSTEM_ASKING, Ordering::Relaxed, Ordering::Relaxed).is_ok() {
|
||||
let ctx = ctx.clone();
|
||||
let spawned = std::thread::Builder::new().name("soundcraft-theme".into()).spawn(move || {
|
||||
let light = matches!(dark_light::detect(), Ok(dark_light::Mode::Light));
|
||||
SYSTEM.store(if light { SYSTEM_LIGHT } else { SYSTEM_DARK }, Ordering::Relaxed);
|
||||
ctx.request_repaint();
|
||||
});
|
||||
if spawned.is_err() {
|
||||
SYSTEM.store(SYSTEM_DARK, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
SYSTEM.load(Ordering::Relaxed) == SYSTEM_LIGHT
|
||||
}
|
||||
|
||||
/// On the web the query is a synchronous `matchMedia` lookup (no threads there).
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn system_is_light(_ctx: &egui::Context) -> bool {
|
||||
if SYSTEM.compare_exchange(SYSTEM_UNKNOWN, SYSTEM_ASKING, Ordering::Relaxed, Ordering::Relaxed).is_ok() {
|
||||
let light = matches!(dark_light::detect(), Ok(dark_light::Mode::Light));
|
||||
SYSTEM.store(if light { SYSTEM_LIGHT } else { SYSTEM_DARK }, Ordering::Relaxed);
|
||||
}
|
||||
SYSTEM.load(Ordering::Relaxed) == SYSTEM_LIGHT
|
||||
}
|
||||
|
||||
/// Track colours tinted for clip bodies.
|
||||
pub fn clip_colors(rgb: [u8; 3], selected: bool) -> (Color32, Color32, Color32) {
|
||||
let [r, g, b] = rgb;
|
||||
@@ -430,8 +389,13 @@ pub fn apply(ctx: &egui::Context, light: bool) {
|
||||
v.widgets.inactive.fg_stroke.color = t.text;
|
||||
v.window_corner_radius = egui::CornerRadius::same(6);
|
||||
v.menu_corner_radius = egui::CornerRadius::same(4);
|
||||
ctx.set_visuals(v);
|
||||
ctx.global_style_mut(|s| {
|
||||
// Keep receiving native OS appearance changes even when our palette is manual. On macOS,
|
||||
// pinning egui's theme also pins the window appearance and hides later OS changes.
|
||||
ctx.set_theme(egui::ThemePreference::System);
|
||||
// Both egui style branches must use our resolved palette: egui can select either branch
|
||||
// as the OS changes, including while SoundCraft's manual Light/Dark choice stays fixed.
|
||||
ctx.all_styles_mut(|s| {
|
||||
s.visuals = v.clone();
|
||||
s.spacing.item_spacing = egui::vec2(6.0, 4.0);
|
||||
s.spacing.button_padding = egui::vec2(6.0, 2.0);
|
||||
s.interaction.tooltip_delay = 0.4;
|
||||
@@ -476,4 +440,78 @@ mod tests {
|
||||
assert_eq!(app.run("ui.theme", json!({"mode": "system"})), Ok(json!({"mode": "system"})));
|
||||
assert!(crate::menus::UI_COMMANDS.iter().any(|c| c.0 == "ui.theme"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn application_frames_follow_system_appearance_and_preserve_manual_palettes() {
|
||||
// Keep all frame assertions in one test: custom-painted tokens share LIGHT_THEME.
|
||||
fn frame(app: &mut SoundApp, ctx: &egui::Context, system_theme: Option<egui::Theme>, light: bool) {
|
||||
let mode = app.ui.theme;
|
||||
let colors = |app: &SoundApp| {
|
||||
app.engine
|
||||
.session()
|
||||
.tracks
|
||||
.iter()
|
||||
.map(|track| {
|
||||
(track.color, track.playlists.iter().flat_map(|playlist| &playlist.clips).map(|clip| clip.color).collect::<Vec<_>>())
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
let before = colors(app);
|
||||
let mut output = ctx.run_ui(egui::RawInput { system_theme, ..Default::default() }, |ui| {
|
||||
let ctx = ui.ctx().clone();
|
||||
app.logic(&ctx);
|
||||
let tokens = if light { Tokens::LIGHT } else { Tokens::DARK };
|
||||
assert_eq!(is_light(), light, "custom palette for {mode:?} with {system_theme:?}");
|
||||
assert_eq!(Tokens::current().panel_bg, tokens.panel_bg);
|
||||
assert_eq!(ctx.options(|options| options.theme_preference), egui::ThemePreference::System);
|
||||
for theme in [egui::Theme::Light, egui::Theme::Dark] {
|
||||
let style = ctx.style_of(theme);
|
||||
assert_eq!(style.visuals.dark_mode, !light);
|
||||
assert_eq!(style.visuals.panel_fill, tokens.panel_bg);
|
||||
assert_eq!(style.visuals.widgets.inactive.bg_fill, tokens.button);
|
||||
assert_eq!(style.visuals.widgets.inactive.fg_stroke.color, tokens.text);
|
||||
assert_eq!(style.spacing.button_padding, egui::vec2(6.0, 2.0));
|
||||
}
|
||||
app.ui(ui);
|
||||
assert_eq!(ui.visuals().panel_fill, tokens.panel_bg);
|
||||
assert_eq!(ui.visuals().widgets.inactive.bg_fill, tokens.button);
|
||||
});
|
||||
output.textures_delta.clear();
|
||||
assert_eq!(app.ui.theme, mode);
|
||||
assert_eq!(serde_json::to_value(&app.ui).unwrap()["theme"], mode.id());
|
||||
assert_eq!(colors(app), before, "appearance must preserve track and clip colors");
|
||||
}
|
||||
|
||||
let appearances = [Some(egui::Theme::Light), Some(egui::Theme::Dark), Some(egui::Theme::Light), None];
|
||||
for initial in [Some(egui::Theme::Light), Some(egui::Theme::Dark), None] {
|
||||
let ctx = egui::Context::default();
|
||||
let mut app = SoundApp::new(soundcraft_engine::Engine::default(), None, Services::default());
|
||||
app.ui = serde_json::from_value(json!({"theme": "system"})).unwrap();
|
||||
frame(&mut app, &ctx, initial, initial == Some(egui::Theme::Light));
|
||||
}
|
||||
|
||||
let ctx = egui::Context::default();
|
||||
let mut engine = soundcraft_engine::demo::demo_engine();
|
||||
let track = engine.session_mut().tracks.first_mut().unwrap();
|
||||
track.color = [19, 83, 147];
|
||||
track.playlists.iter_mut().flat_map(|playlist| &mut playlist.clips).next().unwrap().color = Some([211, 57, 99]);
|
||||
let mut app = SoundApp::new(engine, None, Services::default());
|
||||
for mode in [ThemeMode::System, ThemeMode::Light, ThemeMode::Dark] {
|
||||
app.run("ui.theme", json!({"mode": mode.id()})).unwrap();
|
||||
for appearance in appearances {
|
||||
let light = mode == ThemeMode::Light || (mode == ThemeMode::System && appearance == Some(egui::Theme::Light));
|
||||
frame(&mut app, &ctx, appearance, light);
|
||||
}
|
||||
if mode != ThemeMode::System {
|
||||
let latest = if mode == ThemeMode::Light { egui::Theme::Dark } else { egui::Theme::Light };
|
||||
frame(&mut app, &ctx, Some(latest), mode == ThemeMode::Light);
|
||||
app.run("ui.theme", json!({"mode": "system"})).unwrap();
|
||||
// Resolve the current context immediately, without another OS appearance event.
|
||||
app.logic(&ctx);
|
||||
assert_eq!(is_light(), latest == egui::Theme::Light);
|
||||
assert_eq!(ctx.global_style().visuals.dark_mode, latest != egui::Theme::Light);
|
||||
frame(&mut app, &ctx, Some(latest), latest == egui::Theme::Light);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -446,11 +446,11 @@ pub fn io_setup(app: &mut SoundApp, ctx: &egui::Context) {
|
||||
let r = ui.text_edit_singleline(&mut edit);
|
||||
ui.ctx().memory_mut(|m| m.data.insert_temp(key, edit.clone()));
|
||||
if r.lost_focus() && edit != name {
|
||||
let _ = app.run("mix.bus_rename", json!({"bus": name, "name": edit}));
|
||||
let _ = app.run("setup.io", json!({"action": "rename_bus", "name": name, "new_name": edit}));
|
||||
}
|
||||
ui.label(fmt);
|
||||
if ui.small_button("Delete").clicked() {
|
||||
let _ = app.run("mix.bus_delete", json!({"bus": name}));
|
||||
let _ = app.run("setup.io", json!({"action": "delete_bus", "name": name}));
|
||||
}
|
||||
ui.end_row();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
use egui::{Event, Id, PointerButton, Pos2, RawInput, Rect, pos2, vec2};
|
||||
use serde_json::json;
|
||||
use soundcraft_ui_egui::{Services, SoundApp, midi_editor};
|
||||
|
||||
fn frame(ctx: &egui::Context, app: &mut SoundApp, events: Vec<Event>) {
|
||||
let raw = RawInput { screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(1600.0, 1000.0))), events, ..Default::default() };
|
||||
ctx.run_ui(raw, |ui| {
|
||||
app.logic(ui.ctx());
|
||||
app.ui(ui);
|
||||
})
|
||||
.textures_delta
|
||||
.clear();
|
||||
}
|
||||
|
||||
fn panel(ctx: &egui::Context) -> Rect {
|
||||
egui::containers::panel::PanelState::load(ctx, Id::new("midi_editor")).unwrap().outer_rect
|
||||
}
|
||||
|
||||
fn button(pos: Pos2, pressed: bool) -> Event {
|
||||
Event::PointerButton { pos, button: PointerButton::Primary, pressed, modifiers: Default::default() }
|
||||
}
|
||||
|
||||
fn check_resize(with_clip: bool) {
|
||||
let mut app = SoundApp::new(soundcraft_engine::demo::demo_engine(), None, Services::default());
|
||||
app.ui.show_midi_editor = true;
|
||||
if with_clip {
|
||||
app.run("edit.select", json!({"tracks": ["Keys"]})).unwrap();
|
||||
}
|
||||
assert_eq!(midi_editor::target_clip(&app).is_some(), with_clip);
|
||||
let ctx = egui::Context::default();
|
||||
for _ in 0..6 {
|
||||
frame(&ctx, &mut app, vec![]);
|
||||
}
|
||||
let initial = panel(&ctx);
|
||||
assert!((initial.height() - 320.0).abs() < 1.0, "initial panel: {initial:?}");
|
||||
let notes = app.engine.session().clone();
|
||||
|
||||
// Drag the shared boundary up and down, then allow several idle frames
|
||||
// to catch a panel that looks resized but snaps back after release.
|
||||
for height in [440.0, 280.0, 200.0] {
|
||||
let before = panel(&ctx);
|
||||
let start = pos2(before.center().x, before.top() + 1.0);
|
||||
let end = pos2(start.x, before.bottom() - height);
|
||||
frame(&ctx, &mut app, vec![Event::PointerMoved(start)]);
|
||||
frame(&ctx, &mut app, vec![button(start, true)]);
|
||||
for step in 1..=6 {
|
||||
frame(&ctx, &mut app, vec![Event::PointerMoved(start.lerp(end, step as f32 / 6.0))]);
|
||||
}
|
||||
frame(&ctx, &mut app, vec![button(end, false)]);
|
||||
for _ in 0..4 {
|
||||
frame(&ctx, &mut app, vec![]);
|
||||
}
|
||||
let after = panel(&ctx);
|
||||
assert!((after.height() - height.max(260.0)).abs() < 1.0, "requested {height}, panel: {after:?}");
|
||||
assert!((after.bottom() - initial.bottom()).abs() < 1.0);
|
||||
assert!(app.edit_layout.timeline[3] <= after.top());
|
||||
}
|
||||
|
||||
// Closing and reopening the dock keeps the chosen height.
|
||||
let resized = panel(&ctx);
|
||||
app.run("window.midi_editor", json!({})).unwrap();
|
||||
frame(&ctx, &mut app, vec![]);
|
||||
app.run("window.midi_editor", json!({})).unwrap();
|
||||
for _ in 0..4 {
|
||||
frame(&ctx, &mut app, vec![]);
|
||||
}
|
||||
assert_eq!(panel(&ctx), resized);
|
||||
assert_eq!(*app.engine.session(), notes);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn midi_panel_resizes_with_a_selected_clip() {
|
||||
check_resize(true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn midi_panel_resizes_without_a_selected_clip() {
|
||||
check_resize(false);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
use egui::os::OperatingSystem;
|
||||
use egui::{Key, Modifiers};
|
||||
use serde_json::json;
|
||||
use soundcraft_ui_egui::{Services, SoundApp, shortcuts};
|
||||
@@ -9,14 +10,17 @@ fn press(ctx: &egui::Context, app: &mut SoundApp, key: Key, modifiers: Modifiers
|
||||
|
||||
#[test]
|
||||
fn ctrl_shift_arrows_nudge_the_selected_clip_gain() {
|
||||
let mut app = SoundApp::new(soundcraft_engine::demo::demo_engine(), None, Services::default());
|
||||
let kick = app.engine.session().track_by_name("Kick").unwrap().clips()[0].id;
|
||||
app.engine.execute("edit.select", &json!({"clips": [kick.0]})).unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
ctx.set_os(egui::os::OperatingSystem::Mac);
|
||||
let ctrl_shift = Modifiers { ctrl: true, shift: true, ..Modifiers::NONE };
|
||||
press(&ctx, &mut app, Key::ArrowUp, ctrl_shift);
|
||||
press(&ctx, &mut app, Key::ArrowUp, ctrl_shift);
|
||||
press(&ctx, &mut app, Key::ArrowDown, ctrl_shift);
|
||||
assert_eq!(app.engine.session().find_clip(kick).unwrap().1.gain_db, 1.0);
|
||||
let pc_ctrl = Modifiers { ctrl: true, command: true, ..Modifiers::NONE };
|
||||
for (os, ctrl) in [(OperatingSystem::Mac, Modifiers::CTRL), (OperatingSystem::Windows, pc_ctrl), (OperatingSystem::Nix, pc_ctrl)] {
|
||||
let mut app = SoundApp::new(soundcraft_engine::demo::demo_engine(), None, Services::default());
|
||||
let kick = app.engine.session().track_by_name("Kick").unwrap().clips()[0].id;
|
||||
app.engine.execute("edit.select", &json!({"clips": [kick.0]})).unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
ctx.set_os(os);
|
||||
let ctrl_shift = Modifiers { shift: true, ..ctrl };
|
||||
press(&ctx, &mut app, Key::ArrowUp, ctrl_shift);
|
||||
press(&ctx, &mut app, Key::ArrowUp, ctrl_shift);
|
||||
press(&ctx, &mut app, Key::ArrowDown, ctrl_shift);
|
||||
assert_eq!(app.engine.session().find_clip(kick).unwrap().1.gain_db, 1.0, "{os:?}");
|
||||
}
|
||||
}
|
||||
|
||||
+22
-2
@@ -1,11 +1,14 @@
|
||||
# Architecture
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** minor (crate diagram brought up to date: video, vst3-host, au-host; device I/O limits) · **Target:** Avid Pro Tools Ultimate 2026.4.1
|
||||
|
||||
SoundCraft is a Cargo workspace of small crates with enforced layering (`cargo xtask layers`).
|
||||
Nothing below the UI knows about egui, so the interface can be replaced.
|
||||
|
||||
```
|
||||
L0 time audio-io midi (standalone: no workspace deps)
|
||||
L1 dsp clap-host (plugins; clap-host is the only unsafe crate)
|
||||
L0 time audio-io midi video (standalone: no workspace deps)
|
||||
L1 dsp clap-host vst3-host au-host (plugins; the three hosts are the only crates
|
||||
allowed `unsafe`, confined to their FFI modules)
|
||||
L2 model (the session document)
|
||||
L3 mix (the mix engine)
|
||||
L4 engine playback (commands, undo, I/O · audio devices)
|
||||
@@ -41,6 +44,16 @@ process in parallel; plugin delay compensation aligns every path. The same engin
|
||||
bounces offline (`render_range`) and realtime playback (`soundcraft_playback::Player`, which owns
|
||||
it on the audio thread and receives new session snapshots through a channel).
|
||||
|
||||
Device I/O today is the system's *default* output and *default* input device, opened as two
|
||||
separate cpal streams; the mix renders 512-sample blocks and resamples to the device rate when
|
||||
they differ. There is no device or buffer-size selection, no ASIO, and no MIDI device layer yet
|
||||
(see [`hardware-parity.md`](hardware-parity.md)). Sources are decoded whole into the `SourcePool`
|
||||
and recordings are captured in memory until stop.
|
||||
|
||||
`soundcraft-video` holds our own H.264, ProRes and Motion JPEG decoders and the ISO-BMFF demuxer
|
||||
for the video track. Hosted plugins (`clap-host`, `vst3-host`, `au-host`) are scanned, created and
|
||||
destroyed off the audio thread; only `process` runs on it.
|
||||
|
||||
The cpal callbacks mark their thread (`soundcraft_playback::mark_audio_thread`). Code that may run
|
||||
there can ask `on_audio_thread()` before doing anything that blocks; the desktop app's logger does,
|
||||
so a `log::` record from the audio thread (a stream error, a full synth event queue, a hosted
|
||||
@@ -52,3 +65,10 @@ by the UI thread (see README › Logs).
|
||||
The desktop app's control channel (JSON lines over TCP) exposes engine commands, inspection,
|
||||
synthetic input and screenshots; `soundcraft-cli mcp` wraps either a headless engine or a running
|
||||
app as an MCP server. See `control-protocol.md` and `mcp.md`.
|
||||
|
||||
## Revision history
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-10 | minor | Added video, vst3-host and au-host to the layer diagram; documented device I/O limits and in-memory media |
|
||||
| 2026-10-05 | major | First architecture overview |
|
||||
|
||||
@@ -64,4 +64,11 @@ soundcraft-cli app --port $P ui.menu.invoke '{"path":"Track > New..."}'
|
||||
|
||||
Offscreen (no window): `cargo run -p soundcraft-ui-egui --example ui_shot -- out.png script.jsonl`,
|
||||
where each script line is a request (`{"method": …, "params": …}`), `{"shot": "path.png"}` or
|
||||
`{"steps": n}`.
|
||||
`{"steps": n}`. Use `--system-theme light|dark|none` to supply the initial OS appearance, and
|
||||
script events such as `{"system_theme": "dark"}` to change it on subsequent frames.
|
||||
|
||||
Appearance defaults to Dark. `ui.theme {"mode":"system"}` follows the appearance supplied by
|
||||
the native or web integration at launch and on later frames, while the saved choice stays System.
|
||||
When the integration cannot report an appearance, System uses Dark. Manual Light and Dark stay
|
||||
fixed when the OS appearance changes. An offscreen render without `--system-theme` supplies no
|
||||
OS appearance and therefore resolves System to Dark.
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
# File-format parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** major (first format inventory against Pro Tools 2026.4.1) · **Target:** Avid Pro Tools Ultimate 2026.4.1
|
||||
|
||||
Every format Pro Tools reads or writes, and ours. The Pro Tools list comes from the document types
|
||||
its bundle declares (`CFBundleDocumentTypes` in Info.plist: names only, nothing else read) and its
|
||||
documented import/bounce options. Ours comes from `crates/audio-io` (`detect_format`, `decode`,
|
||||
`ENCODE_EXTENSIONS`), `crates/engine/src/cmd/file.rs`, `crates/engine/src/score.rs`,
|
||||
`crates/engine/src/clip_group_file.rs` and `crates/video`.
|
||||
|
||||
**File formats: ~40 % ready** (estimated: weighted by how often a Pro Tools user hits each row;
|
||||
session interchange carries a third of the weight and is at 0 %). Remaining ≈ 70–120 h.
|
||||
|
||||
Legend: ✅ supported and tested · 🟡 partial · ❌ missing · — not applicable.
|
||||
|
||||
## Sessions and interchange
|
||||
|
||||
| Format | Pro Tools | Read | Write | Tests / notes |
|
||||
|---|---|---|---|---|
|
||||
| Pro Tools session `.ptx` (and `.ptf` from 7–9) | native | ❌ | ❌ | Proprietary, undocumented. Owner decision needed on black-box format study (G-03) |
|
||||
| Session templates `.ptxt` | read/write | ❌ | ❌ | We have no templates at all (G-16) |
|
||||
| AAF (Advanced Authoring Format) | import/export | ❌ | ❌ | #40. The post-production handoff from picture editors (G-03) |
|
||||
| OMF | import/export | ❌ | ❌ | Legacy, still delivered by some editors |
|
||||
| Media Composer–compatible session | export | ❌ | ❌ | AAF variant |
|
||||
| ADM BWF (Dolby Atmos master) | import/export | ❌ | ❌ | G-12 |
|
||||
| SoundCraft session `.scraft` + `Audio Files/` | — | ✅ | ✅ | JSON (serde); mutated-file property test loads or fails cleanly; plugin state embedded |
|
||||
| Import Session Data (from another session) | Pro Tools sessions | 🟡 | — | From `.scraft` only |
|
||||
| Session info as text | export | — | ✅ | `file.export_session_text` |
|
||||
| Clip groups `.rgrp` / ours `.scgrp` | read/write | 🟡 | 🟡 | Our own `.scgrp` format; Pro Tools' clip-group files not read |
|
||||
| Track presets, fade presets, I/O settings, workspace, sync settings | read/write | ❌ | ❌ | Their equivalents don't exist yet (G-16) |
|
||||
|
||||
## Audio
|
||||
|
||||
| Format | Pro Tools | Read | Write | Tests / notes |
|
||||
|---|---|---|---|---|
|
||||
| WAV / BWF (PCM 16/24/32, float 32), `bext` | native | ✅ | ✅ | Native reader/writer, TPDF dither option; also 8-bit and float 64 read |
|
||||
| RF64 / BW64 (> 4 GiB) | read/write | ✅ | ✅ | `encode_wav_rf64` |
|
||||
| WAV `WAVE_FORMAT_EXTENSIBLE` multichannel with channel mask | read/write | ✅ | ✅ | `encode_with_channel_mask`; default 5.1 import creates a stereo track (#89, PR #128) |
|
||||
| AIFF / AIFF-C | native | ✅ | ✅ (`fl32`) | Native |
|
||||
| FLAC | import | ✅ | ✅ | Own encoder |
|
||||
| MP3 | import, bounce | ✅ (symphonia) | ❌ | G-21 |
|
||||
| AAC / M4A / MP4 audio | import, bounce | ✅ (symphonia) | ❌ | G-21 |
|
||||
| ALAC | import | ✅ | ❌ | |
|
||||
| Ogg Vorbis | — | ✅ | ❌ | Beyond Pro Tools |
|
||||
| CAF | — | ✅ | ❌ | |
|
||||
| Sound Designer / SD II | import (legacy) | ❌ | — | Rare today |
|
||||
| ReCycle / REX | import | ❌ | — | Loops |
|
||||
| MXF audio (OP-Atom, OP1a) | import | ❌ | ❌ | Avid media; FilmCraft has an MXF reader to learn from |
|
||||
| iXML / polyphonic field-recorder metadata | read | ❌ | ❌ | G-17 |
|
||||
| Peak overviews | `.wfm` cache | ✅ (own, in memory) | — | `audio-io::peaks`; not cached on disk |
|
||||
|
||||
## MIDI and notation
|
||||
|
||||
| Format | Pro Tools | Read | Write | Tests / notes |
|
||||
|---|---|---|---|---|
|
||||
| Standard MIDI File (type 0/1) | import/export | ✅ | 🟡 | Exported MIDI omits imported controllers and ignores the tracks filter (#87, #98; PR #125) |
|
||||
| MusicXML (to Sibelius) | export | — | ✅ | `file.export_sibelius`, `score.rs` |
|
||||
| Printable score | print | — | ✅ (SVG) | |
|
||||
|
||||
## Video
|
||||
|
||||
| Format | Pro Tools | Read | Write | Tests / notes |
|
||||
|---|---|---|---|---|
|
||||
| QuickTime / MP4, H.264 | import, bounce | ✅ | ❌ | Own pure-Rust decoder (Baseline/Main/High); mutated-movie fuzzing |
|
||||
| ProRes (all flavours) | import | ✅ | ❌ | Own decoder |
|
||||
| Motion JPEG | import | ✅ | ❌ | |
|
||||
| HEVC, AV1 | import | ❌ | ❌ | G-17 |
|
||||
| DNxHD / DNxHR, MXF video | import | ❌ | ❌ | G-17 |
|
||||
| Bounce with video | export | — | ❌ | G-17 |
|
||||
|
||||
## Plugin and preset files
|
||||
|
||||
| Format | Pro Tools | Ours |
|
||||
|---|---|---|
|
||||
| AAX plug-ins | the only format it hosts | ❌ (cannot: Avid SDK licence, PACE signing) |
|
||||
| VST3 / Audio Units / CLAP | — (AAX only) | ✅ hosted (ahead of Pro Tools) |
|
||||
| Plug-in settings `.tfx` | read/write | ❌ (our own JSON presets per plugin) |
|
||||
|
||||
## Revision history
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-10 | major | Created: format inventory against Pro Tools 2026.4.1's declared document types and import/bounce options |
|
||||
+200
@@ -0,0 +1,200 @@
|
||||
# Where SoundCraft falls short of Pro Tools
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** minor (alpha blockers marked after the core-workflow gate) · **Target:** Avid Pro Tools Ultimate 2026.4.1
|
||||
|
||||
Every known shortfall, one entry each, ranked by how much it stops a Pro Tools user from doing
|
||||
real work in SoundCraft. This is the work list: pick from the top. Rows marked **alpha blocker**
|
||||
fail the core-workflow gate in [`roadmap.md`](roadmap.md#alpha-gate) and keep SoundCraft pre-alpha. Numbers and weights live in
|
||||
[`target-app-parity.md`](target-app-parity.md); the plan that orders these into milestones is
|
||||
[`roadmap.md`](roadmap.md). Hours are Opus 5.5 agent-hours (calibration in target-app-parity.md).
|
||||
Estimates marked "≈" are judgements; issue numbers are `storytold/soundcraft` GitHub issues.
|
||||
|
||||
When you close a gap, delete its entry (or shrink it) in the same PR, and bump the timestamp.
|
||||
|
||||
## Ranked gaps
|
||||
|
||||
| # | Gap | Dimension | Doc | Est. (h) |
|
||||
|---|---|---|---|---:|
|
||||
| G-01 | **Alpha blocker.** No audio device, channel or buffer-size selection; no duplex low-latency path | Hardware | [hardware-parity.md](hardware-parity.md) | 25–40 |
|
||||
| G-02 | **Alpha blocker.** No MIDI device input or output | Hardware / MIDI | [hardware-parity.md](hardware-parity.md) | 20–35 |
|
||||
| G-03 | **Alpha blocker.** Cannot exchange sessions with Pro Tools (no AAF/OMF, no `.ptx`) | File formats | [file-format-parity.md](file-format-parity.md) | 50–90 |
|
||||
| G-04 | Open correctness bugs in features counted as done (~25 issues) | Stability | this file | 25–40 |
|
||||
| G-05 | (Entitlement part: alpha blocker for the mix row) Third-party VST3s blocked in the signed macOS build; AU editors missing | Ecosystem | [plugin-parity.md](plugin-parity.md) | 15–25 |
|
||||
| G-06 | (Record-to-disk part: alpha blocker for the tracking row) Sources decoded whole into RAM; recording into RAM | Performance | [hardware-parity.md](hardware-parity.md) | 15–25 |
|
||||
| G-07 | Nothing is compared against Pro Tools' output (no fidelity harness) | Features | this file | 20–30 |
|
||||
| G-08 | Built-in plugin catalogue thin and unverified | Features | [plugin-parity.md](plugin-parity.md) | 80–120 |
|
||||
| G-09 | English only, no string catalog, no CJK/RTL rendering | Localization | [localization-parity.md](localization-parity.md) | 70–110 |
|
||||
| G-10 | No control surfaces (HUI/MCU, EUCON) | Hardware | [hardware-parity.md](hardware-parity.md) | 40–70 |
|
||||
| G-11 | No external sync (MTC, LTC, MIDI clock, video reference) | Hardware | [hardware-parity.md](hardware-parity.md) | 15–25 |
|
||||
| G-12 | Immersive delivery: no Dolby Atmos/ADM BWF, MPEG-H, binaural | Features | [file-format-parity.md](file-format-parity.md) | 50–80 |
|
||||
| G-13 | Sidechain inputs, ARA, out-of-process plugin sandboxing | Ecosystem | [plugin-parity.md](plugin-parity.md) | 30–50 |
|
||||
| G-14 | Windows/Linux/Web exercised only lightly at runtime; no PR CI running the tests | Platforms / Stability | this file | 20–40 |
|
||||
| G-15 | Elastic Audio depth: one algorithm, no Elastic Pitch | Features | [target-app-parity.md](target-app-parity.md) | 30–45 |
|
||||
| G-16 | Session templates, track presets, workspace browser, batch rename | Features | [target-app-parity.md](target-app-parity.md) | 25–40 |
|
||||
| G-17 | Video: HEVC/DNx decode, video export, external timecode | Features / Formats | [file-format-parity.md](file-format-parity.md) | 30–50 |
|
||||
| G-18 | Speech-to-text transcription | AI | this file | 30–50 |
|
||||
| G-19 | UI feel: generic plugin windows, shortcut collisions, pro-grade meters and scaling | UI/UX | [ui-parity.md](ui-parity.md) | 60–100 |
|
||||
| G-20 | MIDI/instrument depth: MIDI learn, sampler, notation editing | Features | [target-app-parity.md](target-app-parity.md) | 40–60 |
|
||||
| G-21 | Compressed export (MP3, AAC/M4A, Ogg/Opus) | File formats | [file-format-parity.md](file-format-parity.md) | 10–20 |
|
||||
| G-22 | Avid cloud services (collaboration, Sketch, Splice, sign-in) | Ecosystem | — | not estimated (owner decision) |
|
||||
|
||||
## Details
|
||||
|
||||
### G-01. Audio interface support
|
||||
|
||||
- **Missing:** choosing the output or input device, the device's channel count and routing to
|
||||
physical channels, the buffer size, and the sample rate from the hardware side. Input and
|
||||
output are separate cpal streams on the *default* devices (`crates/playback/src/lib.rs`
|
||||
`open_device`, `record.rs`); the mix runs in fixed 512-sample blocks and resamples when rates
|
||||
differ. `setup.playback_engine` stores `device`/`buffer_size` params, but playback ignores them.
|
||||
No ASIO on Windows (WASAPI shared only), so latency there is high.
|
||||
- **Evidence:** code above; Playback Engine window says "Mix block size: 512 samples"; issue #42
|
||||
(no input device selection, forced monitoring feedback with virtual routing).
|
||||
- **Impact:** blocks tracking with any multichannel interface; latency makes overdubs hard.
|
||||
- **Estimate:** ≈ 25–40 h (device enumeration and selection, buffer size, duplex stream with one
|
||||
clock, ASIO feature build, per-channel I/O mapping into I/O Setup, latency reporting and
|
||||
record-offset compensation). Needs hands-on hardware testing.
|
||||
|
||||
### G-02. MIDI devices
|
||||
|
||||
- **Missing:** all MIDI hardware I/O. No `midir`/CoreMIDI/WinMM/ALSA sequencer code exists;
|
||||
`setup.midi_input_devices` and `setup.midi_studio` only store names. You cannot record from a
|
||||
keyboard, play a hosted instrument live, drive external synths, or send MIDI clock/MTC.
|
||||
`midi_editor.rs:288` notes audition "needs a live MIDI input path".
|
||||
- **Impact:** MIDI production is limited to drawing notes and importing SMF.
|
||||
- **Estimate:** ≈ 20–35 h (device layer, realtime input to armed tracks and instruments with
|
||||
timestamps, MIDI record/merge/loop, thru, output tracks, clock out).
|
||||
|
||||
### G-03. Session interchange with Pro Tools
|
||||
|
||||
- **Missing:** opening or saving `.ptx`/`.ptf`, AAF and OMF import and export (#40), Media
|
||||
Composer–compatible sessions. Our session format is `.scraft` JSON.
|
||||
- **Impact:** the standard's main-format bar for beta. A Pro Tools user cannot bring a project in
|
||||
or hand one back; post-production (picture editors deliver AAF) is blocked.
|
||||
- **Estimate:** AAF/OMF import + export ≈ 50–90 h (FilmCraft already writes AAF/OMF; we can learn
|
||||
from it but not share code). `.ptx` is proprietary and undocumented: owner decision on whether
|
||||
black-box format study is in the clean-room rules; not included in the estimate.
|
||||
|
||||
### G-04. Correctness bugs in "done" features
|
||||
|
||||
Open on 2026-10-10 (several have PRs open):
|
||||
editing #3, #5, #140, #144; clip/session #86 (PR #123), #88 (PR #130), #91 (PR #124), #92 (PR #132);
|
||||
automation #104 (PR #134); mix/bounce #59 (PR #131), #103 (PR #133), #152; playback #101 (PR #129);
|
||||
MIDI #87, #98 (PR #125); import #89 (PR #128); CLI #95, #96 (PR #126, #127); MCP #145 (PR #146);
|
||||
web #63, #64, #136; Clip List #4 (PR #75); plugin view #6; shortcuts #7, #41;
|
||||
launch #142; Linux #45, #107.
|
||||
- **Impact:** each one is small, but together they say "presence is not parity": an outside
|
||||
contributor found seven in one sitting (#116–#122).
|
||||
- **Estimate:** ≈ 25–40 h at 0.5–1.5 h each, plus review of the open PRs.
|
||||
|
||||
### G-05. Plugin hosting on macOS
|
||||
|
||||
- **Missing:** the `com.apple.security.cs.disable-library-validation` entitlement in the signed
|
||||
build, so hardened runtime refuses third-party VST3 bundles (#153, #149). AU editor views
|
||||
(`crates/au-host/src/lib.rs` header: "Not hosted yet: Audio Unit editor views"). Embedded CLAP
|
||||
GUIs; VST3 editors on Windows/Linux.
|
||||
- **Estimate:** ≈ 15–25 h (entitlement ≈ 1 h; AU Cocoa views and VST3 HWND/X11 views most of it).
|
||||
|
||||
### G-06. Memory-bound media
|
||||
|
||||
- **Missing:** streaming sources from disk; recording to disk. Every source is decoded whole into
|
||||
the `SourcePool`; takes accumulate in RAM until stop (PR #84 open).
|
||||
- **Impact:** hour-long multitrack sessions and long takes exhaust memory; a crash during a long
|
||||
take loses it.
|
||||
- **Estimate:** ≈ 15–25 h.
|
||||
|
||||
### G-07. Fidelity harness against Pro Tools
|
||||
|
||||
- **Missing:** any measured comparison: null tests of bounces of the same material, fade curve
|
||||
and pan-law checks, plugin frequency/dynamics responses, elastic-audio quality, automation
|
||||
timing. Pro Tools is installed; it can be driven black-box with synthetic sessions (outputs kept
|
||||
under `plan/`, never committed).
|
||||
- **Estimate:** ≈ 20–30 h for the harness and the first 20 scored cases; turns many "≈" in
|
||||
target-app-parity.md into measurements.
|
||||
|
||||
### G-08. Built-in plugins
|
||||
|
||||
See [`plugin-parity.md`](plugin-parity.md): 26 processors + 2 instruments against Pro Tools'
|
||||
roughly 80 bundled processors and instruments; no fidelity checks; generic parameter UIs; EQ
|
||||
parametric request #139. ≈ 80–120 h.
|
||||
|
||||
### G-09. Localization
|
||||
|
||||
See [`localization-parity.md`](localization-parity.md): no catalog, every string literal in code;
|
||||
egui has no complex-script shaping or RTL; no CJK font fallback (PR #141 open). ≈ 70–110 h plus
|
||||
native-speaker review.
|
||||
|
||||
### G-10. Control surfaces
|
||||
|
||||
None: no HUI or Mackie Control, no EUCON (Avid-proprietary, needs Avid's SDK: owner decision),
|
||||
no generic MIDI controller mapping. Pro Tools users on S1/S3/S4/S6/Dock or HUI devices can't
|
||||
switch. ≈ 40–70 h for HUI/MCU plus a mapping layer; hardware needed to verify.
|
||||
|
||||
### G-11. Sync
|
||||
|
||||
No MTC/LTC chase or generation, no MIDI Beat Clock (`setup.midi_beat_clock` stores a flag only),
|
||||
no video reference or word clock awareness, no Satellite-style linking. ≈ 15–25 h after G-01/G-02.
|
||||
|
||||
### G-12. Immersive delivery
|
||||
|
||||
No Dolby Atmos renderer integration, ADM BWF import/export, re-renders, binaural monitoring,
|
||||
MPEG-H (new in Pro Tools 2026.4), surround/object automation. ≈ 50–80 h; Dolby's renderer and
|
||||
SDKs are licensed, so we'd write our own ADM and object renderer.
|
||||
|
||||
### G-13. Plugin ecosystem depth
|
||||
|
||||
No sidechain (key input) buses for any host; no ARA (Melodyne-style editing is a menu in Pro
|
||||
Tools); plugins run in-process, so a crashing plugin can take the session down. ≈ 30–50 h. AAX
|
||||
cannot be hosted (#109): Avid's AAX SDK licence and PACE signing; owner decision, likely never.
|
||||
|
||||
### G-14. Platforms and CI
|
||||
|
||||
Only `freebsd.yml` and `windows-arm64.yml` run on pull requests, and only for manifest paths;
|
||||
nothing runs `cargo xtask ci` on PRs. Windows and Linux have startup and rendering reports
|
||||
(#41, #45, #107, #142). ≈ 20–40 h (a PR CI matrix plus runtime smoke tests on three OSes).
|
||||
|
||||
### G-15. Elastic Audio depth
|
||||
|
||||
One warp algorithm; Pro Tools offers several per track (polyphonic, rhythmic, monophonic,
|
||||
varispeed, high-quality offline) plus Elastic Pitch and warp-marker editing depth. ≈ 30–45 h.
|
||||
|
||||
### G-16. Session management
|
||||
|
||||
No session templates, track presets, workspace/sound browser with previews, batch rename (new in
|
||||
2026.4), disk-usage and relink depth. ≈ 25–40 h.
|
||||
|
||||
### G-17. Video and post
|
||||
|
||||
No HEVC, DNxHD/HR or AV1 decode; no bounce with video; no external timecode/9-pin; no iXML
|
||||
field-recorder conform. ≈ 30–50 h.
|
||||
|
||||
### G-18. Speech-to-text
|
||||
|
||||
Pro Tools transcribes dialogue to a searchable transcript (upgraded in 2026.4). We have a
|
||||
settings command only. Needs an openly licensed local speech model. ≈ 30–50 h.
|
||||
|
||||
### G-19. UI feel
|
||||
|
||||
See [`ui-parity.md`](ui-parity.md). ≈ 60–100 h.
|
||||
|
||||
### G-20. MIDI and instrument depth
|
||||
|
||||
MIDI learn, a sampler (PR #108), drum machine depth, notation editing (tuplets, lyrics,
|
||||
articulations, parts). ≈ 40–60 h.
|
||||
|
||||
### G-21. Compressed export
|
||||
|
||||
Bounce writes WAV/BWF/RF64, AIFF and FLAC; Pro Tools also writes MP3 and AAC/M4A in Bounce Mix.
|
||||
≈ 10–20 h (pure-Rust encoders).
|
||||
|
||||
### G-22. Avid cloud services
|
||||
|
||||
Collaboration, cloud projects, Sketch, Splice, Learn, sign-in: 11 of the 30 menu leaves still
|
||||
missing. Out of scope unless the owner decides otherwise.
|
||||
|
||||
## Revision history
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-10 | minor | Marked the alpha blockers (G-01, G-02, G-03; parts of G-05 and G-06) from the core-workflow gate |
|
||||
| 2026-10-10 | major | Created from the full re-measure against Pro Tools Ultimate 2026.4.1: 22 ranked gaps with evidence and estimates |
|
||||
@@ -0,0 +1,75 @@
|
||||
# Hardware parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** major (first hardware audit against Pro Tools 2026.4.1) · **Target:** Avid Pro Tools Ultimate 2026.4.1
|
||||
|
||||
A DAW is judged by how it drives audio interfaces, MIDI gear, control surfaces and sync. This is
|
||||
SoundCraft's weakest dimension. Ours is measured from `crates/playback` (cpal) and the absence of
|
||||
any MIDI-device or control-surface code; Pro Tools' from its documentation and Setup menus.
|
||||
|
||||
**Hardware: ~12 % ready** (estimated). **Performance: ~35 %** (estimated; nothing benchmarked
|
||||
against Pro Tools). Remaining ≈ 110–180 h for hardware, 40–70 h for performance.
|
||||
|
||||
Legend: ✅ works · 🟡 partial · ❌ missing.
|
||||
|
||||
## Audio interfaces
|
||||
|
||||
| Feature | Pro Tools (macOS / Windows) | SoundCraft macOS | Windows | Linux | FreeBSD | Web |
|
||||
|---|---|---|---|---|---|---|
|
||||
| Driver API | Core Audio / ASIO | 🟡 Core Audio via cpal | 🟡 WASAPI shared via cpal; no ASIO | 🟡 ALSA/Pulse via cpal; no JACK | 🟡 cpal (ALSA layer) | 🟡 Web Audio |
|
||||
| Choose playback device | ✅ Playback Engine | ❌ default output only | ❌ | ❌ | ❌ | ❌ |
|
||||
| Choose input device | ✅ | ❌ default input only (#42) | ❌ | ❌ | ❌ | ❌ |
|
||||
| Buffer size (32–2048) | ✅ | ❌ fixed 512-sample mix block | ❌ | ❌ | ❌ | ❌ |
|
||||
| Duplex stream on one clock | ✅ | ❌ separate input/output streams | ❌ | ❌ | ❌ | ❌ |
|
||||
| Hardware I/O mapping (I/O Setup to physical channels) | ✅ | 🟡 multichannel output in order; no per-channel map | 🟡 | 🟡 | 🟡 | ❌ |
|
||||
| Low-latency monitoring | ✅ | ❌ (`options.low_latency_monitoring` is a stored flag) | ❌ | ❌ | ❌ | ❌ |
|
||||
| Latency reporting and record offset compensation | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||
| Sample rate follows the session; resample when it can't | ✅ (switches device) | 🟡 resamples (input pitch bug fixed, #28) | 🟡 | 🟡 | 🟡 | 🟡 |
|
||||
| Avid HDX / HD Native DSP, Hybrid Engine DSP mode | ✅ | ❌ (out of reach: Avid hardware) | ❌ | ❌ | ❌ | ❌ |
|
||||
| Disk allocation / record to disk | ✅ | ❌ recording into RAM (PR #84) | ❌ | ❌ | ❌ | ❌ |
|
||||
|
||||
## MIDI
|
||||
|
||||
| Feature | Pro Tools | SoundCraft (all platforms) |
|
||||
|---|---|---|
|
||||
| MIDI input devices (keyboards, pads) | ✅ | ❌ no MIDI device layer at all (`setup.midi_input_devices` stores names) |
|
||||
| MIDI output to external gear | ✅ | ❌ |
|
||||
| MIDI Thru, input filter | ✅ | ❌ (stored flags only) |
|
||||
| MIDI Beat Clock out, MTC generate/chase | ✅ | ❌ |
|
||||
| MIDI learn / controller mapping | ✅ (via surfaces and plug-in maps) | ❌ |
|
||||
| Virtual MIDI keyboard | ✅ (MIDI Keyboard window) | 🟡 window exists, no live audition path |
|
||||
|
||||
## Control surfaces
|
||||
|
||||
| Feature | Pro Tools | SoundCraft |
|
||||
|---|---|---|
|
||||
| EUCON (S1/S3/S4/S6, Dock, Control app) | ✅ | ❌ Avid-proprietary SDK; owner decision |
|
||||
| HUI | ✅ | ❌ |
|
||||
| Mackie Control Universal | ✅ (via HUI emulation and personalities) | ❌ |
|
||||
| Generic MIDI controller personalities | ✅ | ❌ |
|
||||
|
||||
## Sync and video hardware
|
||||
|
||||
| Feature | Pro Tools | SoundCraft |
|
||||
|---|---|---|
|
||||
| LTC / MTC chase, 9-pin machine control | ✅ (Ultimate) | ❌ |
|
||||
| Word clock / video reference awareness | ✅ via interface | ❌ |
|
||||
| Video output to a second display or video card | ✅ | ❌ (video window only) |
|
||||
| Satellite linking of systems | ✅ | ❌ |
|
||||
|
||||
## Performance
|
||||
|
||||
| Feature | Pro Tools | SoundCraft | Evidence |
|
||||
|---|---|---|---|
|
||||
| Parallel mixing across cores | ✅ | ✅ | `soundcraft-mix` renders independent strips in parallel (rayon) |
|
||||
| Plugin delay compensation | ✅ | ✅ | |
|
||||
| Disk streaming of long sources | ✅ | ❌ | Whole files decoded into the `SourcePool` |
|
||||
| Track count (Ultimate: 2,048 voices) | ✅ | unmeasured | No benchmark exists; add `cargo xtask bench` |
|
||||
| Real-time audio thread discipline | ✅ | ✅ | No blocking/allocation in steady state, audio-thread-aware logger |
|
||||
| Freeze / commit to save CPU | ✅ | 🟡 | `track.commit` renders; `track.freeze` only sets a flag (no CPU saved) |
|
||||
| GPU-accelerated UI | — | ✅ (wgpu/glow via eframe) | llvmpipe X11 issue #107 |
|
||||
|
||||
## Revision history
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-10 | major | Created: audit of audio interface, MIDI, control-surface, sync and performance support against Pro Tools 2026.4.1 |
|
||||
@@ -0,0 +1,42 @@
|
||||
# Localization parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** major (first per-language status) · **Target:** Avid Pro Tools Ultimate 2026.4.1
|
||||
|
||||
SoundCraft is English only. There is no string catalog: every menu label, dialog and tooltip is a
|
||||
literal in Rust (`crates/engine/src/cmd/*`, `crates/ui-egui/src/*`). Pro Tools ships its UI in
|
||||
**8 languages** (bundle `.lproj` folders: English, German, Spanish, French, Japanese, Korean,
|
||||
Simplified Chinese, Traditional Chinese).
|
||||
|
||||
**Localization: ~12 %** (measured: 1 of the target's 8 UI languages, and English is the only one
|
||||
complete). Remaining ≈ 70–110 h: a catalog and extraction pass (≈ 15–25 h), a font fallback for
|
||||
CJK/Devanagari/Arabic (PR #141 open), RTL layout and complex-script shaping (egui has neither:
|
||||
≈ 25–40 h, or ship Arabic/Hindi menus-only), then ≈ 3–5 h of agent translation per language, plus
|
||||
native-speaker review (human).
|
||||
|
||||
Open work: PR #143 (French and Spanish, a Language setting), PR #141 (bundled multilingual font
|
||||
fallbacks), issue #67 (Ukrainian).
|
||||
|
||||
| Language | Code | UI strings translated | Dialogs / tooltips / help | Script support | Native review | Status | Est. to full (h) | In Pro Tools |
|
||||
|---|---|---|---|---|---|---|---:|---|
|
||||
| English | en | all (source) | ✅ | Latin ✅ | — | **full** | 0 | ✅ |
|
||||
| Simplified Chinese | zh-Hans | 0 (0 %) | ❌ | ❌ no CJK font fallback, no IME testing | no | none | 6–10 | ✅ |
|
||||
| Spanish | es | 0 (0 %) — PR #143 pending | ❌ | Latin ✅ | no | none | 3–5 | ✅ |
|
||||
| Hindi | hi | 0 (0 %) | ❌ | ❌ no Devanagari font or shaping | no | none | 8–15 | ❌ |
|
||||
| Arabic | ar | 0 (0 %) | ❌ | ❌ no RTL layout, no shaping | no | none | 15–25 | ❌ |
|
||||
| French | fr | 0 (0 %) — PR #143 pending | ❌ | Latin ✅ | no | none | 3–5 | ✅ |
|
||||
| Portuguese | pt | 0 (0 %) | ❌ | Latin ✅ | no | none | 3–5 | ❌ |
|
||||
| Indonesian | id | 0 (0 %) | ❌ | Latin ✅ | no | none | 3–5 | ❌ |
|
||||
| Japanese | ja | 0 (0 %) | ❌ | ❌ no CJK font fallback, no IME testing | no | none | 6–10 | ✅ |
|
||||
| German | de | 0 (0 %) | ❌ | Latin ✅ | no | none | 3–5 | ✅ |
|
||||
| Korean | ko | 0 (0 %) | ❌ | ❌ no Hangul font fallback | no | none | 6–10 | ✅ |
|
||||
| Vietnamese | vi | 0 (0 %) | ❌ | 🟡 Latin with stacked diacritics, untested | no | none | 3–5 | ❌ |
|
||||
|
||||
Other languages shipped: **0**. (Pro Tools also ships Traditional Chinese, zh-Hant.)
|
||||
|
||||
Per-language hours exclude the shared catalog and script work above.
|
||||
|
||||
## Revision history
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-10 | major | Created: English only, no catalog; the twelve key languages at 0 %; Pro Tools' 8 languages listed from its bundle |
|
||||
@@ -1,6 +1,8 @@
|
||||
# Parity report
|
||||
|
||||
Generated by `soundcraft-cli parity --write docs/parity.md` (also `cargo xtask parity`).
|
||||
> **Generated:** 2026-10-06 (last run of `cargo xtask parity`; the catalog and aliases are unchanged since) · **Target:** Avid Pro Tools Ultimate 2026.4.1 menu tree
|
||||
|
||||
Generated by `soundcraft-cli parity --write docs/parity-checklist.md` (also `cargo xtask parity`).
|
||||
It compares the incumbent's menu tree (feature names observed black-box, `crates/engine/catalog/menus.txt`) with SoundCraft's command registry.
|
||||
UI-layer commands (windows, AudioSuite) are counted by the app's own report (`engine.parity` over the control channel).
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
# Plugin parity: built-in processors and plugin hosting
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** major (first plugin checklist against Pro Tools 2026.4.1) · **Target:** Avid Pro Tools Ultimate 2026.4.1
|
||||
|
||||
Pro Tools users live in plugins: the bundled set covers everyday mixing, and third-party plugins
|
||||
cover the rest. This doc tracks both. Our plugins have our own names and are written from
|
||||
scratch (clean-room rule): Avid plugin names are not used here, only categories.
|
||||
|
||||
**Built-in plugins and AudioSuite: breadth ~40 %, ready ~35 %** (80–120 h).
|
||||
**Third-party hosting: breadth ~70 %, ready ~45 %** (40–60 h). Both estimated.
|
||||
|
||||
## Built-in processors (measured from `crates/dsp/src/plugins`)
|
||||
|
||||
| Category | Pro Tools bundles (approx., by category) | SoundCraft (ids) | Gap |
|
||||
|---|---|---|---|
|
||||
| EQ | 1-band and 7-band EQ, channel strip, vintage EQ models | `eq_1band`, `eq_7band`, `channel_strip` | Vintage models, linear phase, dynamic EQ; parametric UI (#139) |
|
||||
| Dynamics | compressor/limiter, expander/gate, de-esser, maximizer, vintage compressors, multiband | `compressor`, `expander_gate`, `de_esser`, `maximizer` | Vintage/opto models, multiband, sidechain input (no key input anywhere) |
|
||||
| Reverb | algorithmic room/plate/hall; convolution (paid tier) | `room_reverb`, `plate_reverb` | Hall/chamber, convolution, early-reflection editor |
|
||||
| Delay | modulated delay, tape/analog delays | `mod_delay` | Tape, ping-pong, multi-tap, tempo-sync depth |
|
||||
| Modulation | chorus, flanger, phaser, tremolo, ring/frequency shift | `chorus`, `flanger`, `phaser` | Tremolo, auto-pan, ring modulation |
|
||||
| Harmonic | lo-fi, rectifier, amp simulation, saturation, tape | `lofi`, `rectifier`, `saturator` | Amp and cabinet models, tape |
|
||||
| Pitch and time | pitch shift, varispeed effects, time shift | `pitch_shifter`, `time_shift` | Formant control, varispeed effect, pitch correction |
|
||||
| Utility | trim, gain, invert, DC offset, signal generator, dither, time adjuster | `trim`, `gain`, `invert`, `dc_offset_removal`, `signal_generator`, `dither` | Time adjuster, meter/analyzer plugins, noise shaping options |
|
||||
| Instruments | several virtual instruments (multi-sampler, piano, drum machine, organ, synths, sample player; Massive X Player added in 2026.4) | `subtractive_synth`, `drum_synth` | Sampler (PR #108 open), piano, organ, wavetable, content library |
|
||||
| **Total** | **≈ 80 processors and instruments** (estimate from Avid's bundle documentation) | **26 processors + 2 instruments** | |
|
||||
|
||||
AudioSuite: every catalog AudioSuite leaf maps to one of our processes (`AUDIOSUITE` in
|
||||
`crates/ui-egui/src/menus.rs`), so the AudioSuite menu is 100 % present. Depth: no preview,
|
||||
handles, "clip by clip / entire selection" and multi-input modes beyond the basics; hosted
|
||||
plugins can't be used offline as AudioSuite yet.
|
||||
|
||||
Fidelity: **unmeasured.** Nothing compares a SoundCraft processor's frequency response, gain
|
||||
reduction curve or reverb tail with the Pro Tools equivalent (G-07 in [`gaps.md`](gaps.md)).
|
||||
User report #152: a saved instrument level does not reach the bounce.
|
||||
|
||||
## Third-party plugin hosting
|
||||
|
||||
| Feature | Pro Tools | SoundCraft | Notes |
|
||||
|---|---|---|---|
|
||||
| Plugin format | AAX only (Native and DSP) | CLAP, VST3, Audio Units (macOS) | Users' libraries mostly ship VST3/AU too. AAX (#109) needs Avid's SDK licence and PACE signing: owner decision, likely never |
|
||||
| VST2, LV2, LADSPA | — | ❌ (#30) | Beyond Pro Tools; VST2 SDK is no longer licensable |
|
||||
| Scan, vendor menus, instrument picker | ✅ | ✅ | #23 |
|
||||
| Audio, parameters, automation, latency (PDC) | ✅ | ✅ | |
|
||||
| Notes / MIDI to instruments | ✅ | ✅ | But no live MIDI input (G-02) |
|
||||
| State saved in sessions, presets | ✅ | ✅ | |
|
||||
| Create/destroy off the audio thread | ✅ | ✅ | |
|
||||
| Editors: CLAP | — | 🟡 floating windows; no embedded GUI | |
|
||||
| Editors: VST3 | — | 🟡 macOS only (verified with a commercial pitch editor) | Windows/Linux views missing |
|
||||
| Editors: AU | — | ❌ | Cocoa view hosting missing |
|
||||
| Multiple editor pages | ✅ | 🟡 | #6 |
|
||||
| macOS hardened-runtime entitlement for third-party code | ✅ | ❌ | #153, #149 — signed builds refuse third-party VST3 |
|
||||
| Sidechain / key input | ✅ | ❌ | All three hosts |
|
||||
| ARA 2 (pitch/time editors in the timeline) | ✅ | ❌ | Menu leaves Track/Clip/Window ▸ ARA still missing |
|
||||
| Crash isolation | in-process (DSP on HDX) | in-process | Out-of-process hosting would beat Pro Tools |
|
||||
| Plugin delay reporting changes on the fly | ✅ | 🟡 | Aux insert latency bug #103 (PR #133) |
|
||||
|
||||
## Revision history
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-10 | major | Created: built-in processor inventory by category and third-party hosting checklist |
|
||||
@@ -50,6 +50,7 @@ web build and are checked, the signing jobs are refused, and no release is draft
|
||||
| Windows 11 ARM64 | `soundcraft-<v>-windows-arm64.msi`, `soundcraft-<v>-windows-arm64-portable.zip` | `windows-latest` (cross-compiled; `windows-arm64.yml` installs and runs it on ARM64) |
|
||||
| Linux x86_64 | `soundcraft-<v>-linux-x86_64.{AppImage,deb,rpm,tar.gz}` | `ubuntu-22.04` |
|
||||
| Linux aarch64 | `soundcraft-<v>-linux-aarch64.{AppImage,deb,rpm,tar.gz}` | `ubuntu-22.04-arm` |
|
||||
| Linux riscv64 | `soundcraft-<v>-linux-riscv64.tar.gz` (glibc 2.39+) | `ubuntu-24.04` (cross-compiled; CLI smoke-tested under QEMU) |
|
||||
| AppImage updates | `soundcraft-<v>-linux-{x86_64,aarch64}.AppImage.zsync` | with the AppImage |
|
||||
| Flatpak x86_64, aarch64 | `soundcraft-<v>-linux-{x86_64,aarch64}.flatpak` | `ubuntu-24.04`, `ubuntu-24.04-arm` (repackages the Linux tarball) |
|
||||
| FreeBSD 14 x86_64 | `soundcraft-<v>-freebsd-x86_64.tar.gz` (a `/usr/local` tree) | FreeBSD 14.3 VM on `ubuntu-latest` |
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
# SoundCraft roadmap: milestones and what's next
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** minor (alpha gate added; stage re-normalized to pre-alpha) · **Target:** Avid Pro Tools Ultimate 2026.4.1
|
||||
|
||||
Forward-looking plan. Status and numbers: [`../ROADMAP.md`](../ROADMAP.md) and
|
||||
[`target-app-parity.md`](target-app-parity.md); the ranked work list: [`gaps.md`](gaps.md).
|
||||
Hours are Opus 5.5 agent-hours.
|
||||
|
||||
## Current focus
|
||||
|
||||
1. **Merge or close the open bug-fix PRs** (#123–#134, #146, #148, #75) and fix the rest of the
|
||||
correctness backlog (G-04). ≈ 15–25 h.
|
||||
2. **Audio interface basics** (G-01): device selection, buffer size, duplex stream, ASIO. ≈ 25–40 h.
|
||||
3. **MIDI devices** (G-02): input to armed tracks and instruments, output, thru, clock. ≈ 20–35 h.
|
||||
4. **macOS plugin hosting** (G-05): library-validation entitlement now (#153), then AU editors. ≈ 15–25 h.
|
||||
5. **A PR CI that runs `cargo xtask ci`** on Linux, macOS and Windows (G-14). ≈ 5–10 h.
|
||||
|
||||
## Alpha gate
|
||||
|
||||
The core workflows a Pro Tools professional does every day, checked end to end on macOS (the
|
||||
main platform). Any **no** keeps SoundCraft **pre-alpha**, and those rows are the alpha checklist
|
||||
(craftrules `standards/progress-docs.md`, Stages).
|
||||
|
||||
| Workflow | Works end to end? | Evidence | Hours to pass |
|
||||
|---|---|---|---:|
|
||||
| Record audio from a chosen interface at a chosen buffer size, monitor, punch, keep the takes | **no** | Default output and input devices only. Fixed 512-sample block. Separate input and output streams. `setup.playback_engine` params ignored by playback. Takes held in RAM until stop (PR #84). No input device choice (#42). | 35–55 |
|
||||
| Record MIDI from a controller and play it back through an instrument | **no** | Playback of drawn and imported MIDI works. No MIDI device layer at all, so nothing can be recorded and instruments can't be played live (G-02). | 20–35 |
|
||||
| Edit: select, trim, fade, comp playlists, Slip/Shuffle/Grid | yes (rough) | Every Edit menu command; property-tested with undo; open bugs #3, #5, #140 don't block the workflow | — |
|
||||
| Mix with built-in and third-party plugins, sends, busses, automation | partial (passes on CLAP/AU) | 26 built-in processors; CLAP and AU host on the signed macOS build, but its missing entitlement blocks third-party VST3 (#153). No AU plugin windows yet; VST3 windows on macOS only | 1–2 (entitlement) |
|
||||
| Bounce mix and stems; save and reopen the session | yes (rough) | WAV/BWF/AIFF/FLAC bounce, stems, `.scraft` save/reopen with plugin state; bugs #59, #103 in bus bounce and aux latency (PRs open) | — |
|
||||
| Exchange a session with Pro Tools or a picture editor | **no** | No `.ptx`, AAF or OMF import or export (#40, G-03) | 50–90 |
|
||||
|
||||
**Result: pre-alpha.** Three workflows fail. Passing them is ~110–180 h, about 70 % of it
|
||||
parallelizable (interfaces, MIDI and AAF are independent crates).
|
||||
|
||||
## Milestones
|
||||
|
||||
Status: ✅ done · 🟡 in progress · ⬜ not started.
|
||||
|
||||
| # | Milestone | Status | Remaining (h) |
|
||||
|---|---|---|---:|
|
||||
| M0–M4 | Workspace, gates, model, I/O, DSP, mix engine, command engine, Edit/Mix UI, playback, metering, plugins, bounce | ✅ | — |
|
||||
| M5 | Editing depth (tools, modes, playlists, fades, comping, clip groups) | ✅ (bugs open, G-04) | in G-04 |
|
||||
| M6 | Recording, punch, loop record, monitoring | 🟡 first cut; device choice and record-to-disk missing | 40–60 |
|
||||
| M7 | MIDI and notation editing | 🟡 | 50–80 |
|
||||
| M8 | CLI, control channel, MCP, agent acceptance test | ✅ | 3 |
|
||||
| M9 | AudioSuite breadth ✅, Elastic Audio 🟡, Beat Detective 🟡 | 🟡 | 40–60 |
|
||||
| M10 | Public releases on all platforms | ✅ v0.3.0 (2026-10-08) and v0.4.0 (2026-10-10) released for every platform | — |
|
||||
| M11 | Third-party plugin hosting (CLAP, VST3, AU), video, surround | 🟡 mostly done | 40–60 |
|
||||
| M12 | **Hardware**: audio interfaces, MIDI devices, sync, control surfaces | ⬜ | 110–180 |
|
||||
| M13 | **Interchange**: AAF/OMF import/export, disk streaming, record to disk | ⬜ | 65–115 |
|
||||
| M14 | **Fidelity harness** against Pro Tools (null tests, plugin responses) | ⬜ | 20–30 |
|
||||
| M15 | **Localization**: catalog, fonts/RTL, the 8 Pro Tools languages first | ⬜ (PRs #141, #143 open) | 70–110 |
|
||||
| M16 | Built-in plugin depth, sidechain, ARA | ⬜ | 110–170 |
|
||||
| M17 | Immersive delivery (ADM BWF, object renderer, binaural) and video depth | ⬜ | 80–130 |
|
||||
|
||||
## To beta (~350–570 h, about 60–70 % parallelizable)
|
||||
|
||||
Beta needs ~75 % ready for real work and no blocking gap in session interchange.
|
||||
|
||||
| Step | Gaps | Est. (h) |
|
||||
|---|---|---:|
|
||||
| Correctness backlog and a PR CI | G-04, G-14 | 40–65 |
|
||||
| Audio interfaces, MIDI devices, sync | G-01, G-02, G-11 | 60–100 |
|
||||
| AAF/OMF interchange, disk streaming, record to disk | G-03, G-06 | 65–115 |
|
||||
| Plugin hosting completeness on every OS, sidechain | G-05, G-13 | 35–60 |
|
||||
| Fidelity harness and the fixes it finds | G-07 | 40–60 |
|
||||
| Built-in plugin essentials (EQ/dynamics/reverb/delay depth, sampler) | G-08 (half) | 40–60 |
|
||||
| UI feel pass: shortcuts, plugin windows, meters | G-19 (half) | 30–50 |
|
||||
| Localization of the 8 Pro Tools languages (catalog, CJK fonts) | G-09 (part) | 40–60 |
|
||||
|
||||
Needs a human: hardware for G-01/G-02/G-10/G-11, native-speaker review, and owner decisions on
|
||||
AAX, `.ptx` and Avid cloud features.
|
||||
|
||||
## After beta (to full parity, a further ~600–900 h)
|
||||
|
||||
Control surfaces (G-10), immersive delivery (G-12), the rest of the built-in plugin catalogue
|
||||
(G-08), Elastic Audio depth (G-15), session templates and browser (G-16), video depth (G-17),
|
||||
speech-to-text (G-18), notation and instrument depth (G-20), compressed export (G-21), and the
|
||||
remaining languages.
|
||||
|
||||
There is no `cargo xtask scorecard` in this repo; the measured number is `cargo xtask parity`
|
||||
([`parity-checklist.md`](parity-checklist.md)).
|
||||
|
||||
## Revision history
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-10 | minor | Added the alpha gate (3 of 6 core workflows fail → pre-alpha) |
|
||||
| 2026-10-10 | major | Created from ROADMAP.md's milestones and alpha list; added M12–M17 and the beta plan with estimates |
|
||||
@@ -0,0 +1,252 @@
|
||||
# Target-app parity: SoundCraft vs Avid Pro Tools
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** minor (hours to ~95 % for each audience; full number confirmed as an additive weighted sum) · **Target:** Avid Pro Tools Ultimate 2026.4.1 (bundle 26.4.1.179, macOS)
|
||||
|
||||
The authoritative assessment of how close SoundCraft is to Pro Tools. [`ROADMAP.md`](../ROADMAP.md)
|
||||
summarizes it, [`gaps.md`](gaps.md) itemizes every shortfall, and the per-dimension docs go deeper:
|
||||
[`ui-parity.md`](ui-parity.md), [`file-format-parity.md`](file-format-parity.md),
|
||||
[`hardware-parity.md`](hardware-parity.md), [`plugin-parity.md`](plugin-parity.md),
|
||||
[`localization-parity.md`](localization-parity.md). The generated menu checklist is
|
||||
[`parity-checklist.md`](parity-checklist.md).
|
||||
|
||||
## Headline
|
||||
|
||||
| Number | Value | Kind |
|
||||
|---|---|---|
|
||||
| Menu breadth (engine + UI layer) | **482 / 512 menu leaves (94.1 %)** | **measured**: the 512-leaf catalog `crates/engine/catalog/menus.txt` against the command registry; engine alone 397 / 512 (77.5 %) from `cargo xtask parity` ([`parity-checklist.md`](parity-checklist.md)); the 85 UI-layer matches re-counted on 2026-10-10 from `crates/ui-egui/src/menus.rs` (`UI_COMMANDS`, `AUDIOSUITE`, `MENU_WINDOWS`, Mix Window Views) by script |
|
||||
| Feature breadth (menus plus everything not in a menu: plugins, hardware, formats, immersive, cloud) | **~70 %** | estimated (area table below, presence only) |
|
||||
| **Ready for real work** (whole of Pro Tools) | **~40 %** | estimated: written dimension weights × values = 40.6 % ([By dimension](#by-dimension)) |
|
||||
| **Mainstream practitioner** (a working tracking/mixing engineer) | **~36 %** | estimated: written area weights and discounts ([below](#mainstream-practitioner-and-essentials-user)) |
|
||||
| **Essentials user** (making a song or podcast with the core features) | **~57 %** | estimated: written feature weights and discounts ([below](#mainstream-practitioner-and-essentials-user)) |
|
||||
| Stage | **pre-alpha** (core-workflow gate fails) | see [Stage](#stage) |
|
||||
| Remaining effort to alpha | **~110–180 Opus 5.5 agent-hours** | estimated: the failing gate rows |
|
||||
| Remaining effort to beta | **~350–570 Opus 5.5 agent-hours** | estimated, calibrated (below) |
|
||||
| Remaining effort to full parity | **~950–1,450 Opus 5.5 agent-hours** | estimated, calibrated (below) |
|
||||
|
||||
### Readiness by audience
|
||||
|
||||
| Audience | Ready % | Opus 5.5 agent wall-clock hours to ~95 % | Work that dominates |
|
||||
|---|---:|---:|---|
|
||||
| Full target (ready for real work) | ~40 % | ~950–1,450 h (~60–70 % parallelizable) | Hardware (interfaces, MIDI, surfaces, sync), the built-in plugin catalogue, AAF/OMF and immersive delivery, localization, fidelity against Pro Tools |
|
||||
| Mainstream practitioner | ~36 % | ~380–600 h (~60 % parallelizable) | Interface recording and MIDI devices, AAF/OMF exchange, stock-plugin depth, the core-path bug backlog, interaction feel |
|
||||
| Essentials user | ~57 % | ~110–200 h (~70 % parallelizable) | Launch and platform fixes, an onboarding and user guide, stock-effect panels and presets, MP3/AAC export, editing and transport bugs |
|
||||
|
||||
Each audience's hours are for its own areas only, so essentials ⊂ mainstream ⊂ full. Calibration
|
||||
is the same as for the full estimate ([Effort and calibration](#effort-and-calibration)): bug fixes
|
||||
at 0.5–1.5 h each, features at about 200–300 tested lines per agent-hour, from the public-week PRs.
|
||||
- **Mainstream sum:** recording 40–60, MIDI 50–80, editing 40–60, mixing 35–50, automation 20–30,
|
||||
stock plugins 80–120, AAF/OMF 50–90, compressed export 10–20, bug backlog 25–40, interaction
|
||||
feel 30–50.
|
||||
- **Essentials sum:** default-device recording 10–15, transport 5–10, editing polish 15–25, effect
|
||||
panels and presets 20–30, MP3/AAC export 10–20, built-in instrument 10–20, launch and platform
|
||||
15–25, onboarding and user guide 15–25, save fixes 5–10.
|
||||
|
||||
The previous figure in ROADMAP.md (2026-10-07: "~65 % overall feature parity", "110–150 h to 100 %")
|
||||
measured feature *presence*. This pass measures readiness, and found new evidence that pulls the
|
||||
numbers down: 37 open GitHub issues from the first public week (about 25 of them correctness bugs
|
||||
in features counted as done), no MIDI hardware input or output at all, no audio device or buffer
|
||||
size selection, no localization, and a macOS release that cannot load signed third-party VST3s
|
||||
(#153). The estimates did not drift up; the scope of what is measured widened.
|
||||
|
||||
## How this was measured
|
||||
|
||||
- **The target.** `/Applications/Pro Tools.app` on the owner's Mac: Info.plist reports version
|
||||
26.4.1.179 (Pro Tools Ultimate 2026.4.1). Only bundle metadata was read (version,
|
||||
`CFBundleDocumentTypes` for the file types it opens, the `.lproj` folder names for its UI
|
||||
languages: en, de, es, fr, ja, ko, zh-Hans, zh-Hant). Nothing inside the bundle's resources,
|
||||
plug-ins or code was read or copied (clean-room rule, AGENTS.md).
|
||||
- **The menu tree.** `plan/protools/menus-clean.txt` (observed black-box, local only) became the
|
||||
committed 512-leaf catalog; `cargo xtask parity` and the UI layer's `engine.parity` score it.
|
||||
- **Avid's own documentation.** *What's New in Pro Tools 2026.4* (Track Pin, MPEG-H renderer,
|
||||
Dolby headphone personalization, speech-to-text upgrades, batch file rename) and earlier
|
||||
release notes for features outside menus (Dolby Atmos renderer integration, ADM BWF, Sketch,
|
||||
cloud collaboration, Splice, ARA, HEAT, control surfaces, I/O setup).
|
||||
- **The code on origin/main (3bc2d92, 2026-10-10).** Read and counted with grep: ~70,900 lines of
|
||||
Rust; 524 `#[test]` functions plus proptest suites; ~350 macro-registered engine command ids;
|
||||
26 built-in processors and 2 instruments in `crates/dsp/src/plugins`; audio I/O in
|
||||
`crates/audio-io`; device I/O in `crates/playback` (cpal default output and default input only);
|
||||
no MIDI device crate (no `midir`/CoreMIDI anywhere); no string catalog.
|
||||
- **Users.** The 37 open and 21 closed GitHub issues, and the 42 merged and 29 open pull
|
||||
requests up to 2026-10-10, are the only field evidence. Nothing has been benchmarked against
|
||||
Pro Tools.
|
||||
- **Not done:** running Pro Tools side by side on the same material (null tests of mixes, plugin
|
||||
responses, fades, elastic audio). That is gap G-01 in [`gaps.md`](gaps.md).
|
||||
|
||||
## By dimension
|
||||
|
||||
Weights are how much each dimension decides whether a Pro Tools user (music production,
|
||||
post-production, tracking) can switch. "Ready" is depth, correctness and fidelity, not presence.
|
||||
|
||||
| Dimension | Weight | Ready | Kind | Remaining (h) | Doc | Evidence |
|
||||
|---|---:|---:|---|---:|---|---|
|
||||
| Features | 45 % | **~45 %** | estimated | 520–810 | [area table](#feature-areas) | Menu breadth 94 % measured, but depth is thin: see the area table |
|
||||
| UI/UX fidelity | 10 % | **~55 %** | estimated | 60–100 | [ui-parity.md](ui-parity.md) | Edit/Mix windows, every menu, floating windows; open UX bugs #3 #5 #6 #7 #140 #144; shortcuts only recently right on Windows/Linux (#15, #41); generic plugin UIs |
|
||||
| File formats | 10 % | **~40 %** | estimated | 70–120 | [file-format-parity.md](file-format-parity.md) | WAV/BWF/RF64/AIFF/FLAC read+write, compressed formats read; no `.ptx`, AAF, OMF, ADM BWF, MP3/AAC export, video export |
|
||||
| Hardware | 10 % | **~12 %** | estimated | 110–180 | [hardware-parity.md](hardware-parity.md) | Default output and default input device only, fixed 512-sample block, no ASIO, no MIDI devices, no control surfaces, no sync |
|
||||
| Stability | 10 % | **~45 %** | estimated | 50–80 | [gaps.md](gaps.md) | Never-crash rules and proptests (random edits, mutated sessions, hostile params); but ~25 open correctness bugs, a launch failure (#142), X11 llvmpipe failure (#107) |
|
||||
| Performance | 5 % | **~35 %** | estimated (unmeasured) | 40–70 | [hardware-parity.md](hardware-parity.md) | Parallel strip rendering and PDC exist; whole files decoded into RAM, recording into RAM (#84 open), no track-count or latency benchmarks |
|
||||
| Ecosystem / plugins | 4 % | **~30 %** | estimated | in Features + Hardware | [plugin-parity.md](plugin-parity.md) | CLAP/VST3/AU hosting; no AAX (Pro Tools' only format), no ARA, no sidechain, no control-surface ecosystem, no content library |
|
||||
| Platforms | 3 % | **~65 %** | estimated | 20–40 | [ROADMAP.md](../ROADMAP.md) | Builds for macOS, Windows x64/x86/arm64, Linux, FreeBSD, Web (Pro Tools: macOS, Windows), but only macOS is exercised at runtime; Web lacks file pickers (#63, #64) |
|
||||
| Localization | 2 % | **~12 %** | measured (catalog count) | 70–110 | [localization-parity.md](localization-parity.md) | English only, no string catalog; Pro Tools ships 8 UI languages; FR/ES PR #143 open |
|
||||
| AI features | 1 % | **~5 %** | estimated | 30–50 | [gaps.md](gaps.md) | Pro Tools has speech-to-text transcription (upgraded in 2026.4); we have only an audio-to-MIDI pitch tracker |
|
||||
| **Weighted** | 100 % | **~40 %** | | **~950–1,450** (overlaps removed) | | |
|
||||
|
||||
Agent control is not weighted (Pro Tools has only its scripting SDK); SoundCraft is ahead there:
|
||||
every command is reachable from the CLI, the JSON control channel and MCP.
|
||||
|
||||
## Mainstream practitioner and essentials user
|
||||
|
||||
Both use the method in craftrules `standards/progress-docs.md`. Each multiplies a weighted depth
|
||||
average by written-down discounts. Depths come from the [Feature areas](#feature-areas) table and
|
||||
the evidence there; issue numbers are `storytold/soundcraft` GitHub issues.
|
||||
|
||||
### Mainstream practitioner: ~36 %
|
||||
|
||||
The typical Pro Tools professional is a music or post engineer who tracks through an audio
|
||||
interface, overdubs MIDI, edits, mixes with the stock plugins and bounces every week. This number
|
||||
leaves out third-party plugin hosting, Avid cloud services and AI, control surfaces and sync,
|
||||
immersive delivery, video, notation, and languages other than English.
|
||||
|
||||
| Area (weekly use) | Weight | Depth |
|
||||
|---|---:|---:|
|
||||
| Recording and monitoring through an interface | 20 % | 40 % |
|
||||
| Editing (trim, fades, comping, Slip/Grid, nudge) | 22 % | 65 % |
|
||||
| Mixing and routing (faders, sends, busses, inserts, PDC) | 18 % | 60 % |
|
||||
| Automation | 10 % | 55 % |
|
||||
| Built-in plugins and AudioSuite | 12 % | 35 % |
|
||||
| MIDI recording, editing, instruments | 10 % | 30 % |
|
||||
| Bounce, save and reopen | 8 % | 60 % |
|
||||
| **Weighted depth** | 100 % | **50.6 %** |
|
||||
|
||||
| Discount | Factor | Evidence |
|
||||
|---|---:|---|
|
||||
| Interaction fidelity | ×0.90 | Shortcut collisions and dead shortcuts (#140 Heal, #144 Mix/Edit vs zoom, #7 Export); Multitool handles don't adjust audio (#5); generic plugin parameter panels (#138, #139); clip deletion inconsistencies (#3, #4) |
|
||||
| Stability on real machines | ×0.92 | 26 open core-path bugs, e.g. bounce and latency (#59, #103), playback speed (#101), session saves (#86, #91), plus a launch failure (#142). Never-crash rules and property tests hold up: no crash reports on macOS since #16 |
|
||||
| File exchange with Pro Tools users | ×0.85 | Engineers trade sessions, not stems. No `.ptx`, AAF or OMF (#40); WAV/BWF stems and MIDI files do work, so the factor isn't lower |
|
||||
| **Result** | 50.6 × 0.90 × 0.92 × 0.85 = **35.6 % ≈ 36 %** | |
|
||||
|
||||
This comes out *below* the full number (~40 %), which the standard says is unusual. The
|
||||
mainstream engineer's weekly core is interface recording and MIDI overdubs, exactly the
|
||||
alpha-gate failures. The full number gets partial credit from areas this user doesn't weight:
|
||||
editing breadth, five platforms built, agent control.
|
||||
|
||||
**User evidence** (GitHub, 2026-10-10; all 69 issues are from 28 outside reporters, none from
|
||||
maintainers):
|
||||
- **Praise:** 0 threads, and no "switched from Pro Tools" reports.
|
||||
- **Open issues:** of 48 open, 26 are core-path bugs (editing, bounce, playback, sessions,
|
||||
shortcuts, plugin loading, recording monitoring), 7 are niche-path bugs (web build, CLI/MCP,
|
||||
Linux fonts and llvmpipe), and 15 are requests.
|
||||
- **Requests:** several ask for other DAWs' feel: Ableton shortcuts and layout (#71, #72), an
|
||||
FL/Ableton port (#38), Audition-style file editing (#81, #82). AAX (#109), VST2/LV2 (#30),
|
||||
Ukrainian (#67).
|
||||
- **Closed:** 21.
|
||||
- **Contributions:** 12 outside contributors have merged PRs in the first week. That's
|
||||
engagement, not yet use on real projects.
|
||||
|
||||
### Essentials user: ~57 %
|
||||
|
||||
A user who needs only the essentials: makes a song, demo or podcast, records through the computer's default
|
||||
microphone or interface, uses default settings, and exports a file to share.
|
||||
|
||||
| Core feature | Weight | Depth | Notes |
|
||||
|---|---:|---:|---|
|
||||
| New session, open a session, demo | 10 % | 80 % | |
|
||||
| Import audio (drag and drop, WAV/MP3/AAC/FLAC) | 10 % | 80 % | Reads every common format |
|
||||
| Record from the default device | 12 % | 65 % | Works on the default input; forced monitoring can feed back (#42) |
|
||||
| Play, stop, loop, scroll, zoom | 10 % | 75 % | Half-speed after Stop (#101) |
|
||||
| Basic editing: select, cut/copy/paste, split, trim, move, fades | 18 % | 70 % | #3, #5 |
|
||||
| Undo/redo | 5 % | 90 % | Every command undoable |
|
||||
| Volume, pan, mute, solo | 10 % | 85 % | |
|
||||
| Add a stock effect (EQ, compressor, reverb) and pick a preset | 10 % | 60 % | Generic panels, few presets |
|
||||
| Draw MIDI notes for a built-in synth | 5 % | 50 % | Two built-in instruments |
|
||||
| Save and reopen | 5 % | 80 % | Web build can drop unsaved edits (#136) |
|
||||
| Export / bounce a file to share | 5 % | 60 % | WAV/AIFF/FLAC only, no MP3/AAC |
|
||||
| **Weighted depth** | 100 % | **72.4 %** | |
|
||||
|
||||
| Discount | Factor | Evidence |
|
||||
|---|---:|---|
|
||||
| Launch and stability | ×0.90 | Doesn't launch for one user (#142), X11 llvmpipe failure (#107), Linux symbols as boxes (#45), Windows shortcuts (#41); macOS is solid |
|
||||
| Discoverability and UI clarity | ×0.90 | Pro Tools' dense layout with no onboarding or user guide; shortcut collisions; plugin windows are bare sliders |
|
||||
| Opening files people send them | ×0.97 | These users get WAV/MP3, which work; only Pro Tools sessions fail |
|
||||
| **Result** | 72.4 × 0.90 × 0.90 × 0.97 = **56.9 % ≈ 57 %** | |
|
||||
|
||||
## Feature areas
|
||||
|
||||
Weights by what Pro Tools users actually spend time on. **Breadth** = the feature exists;
|
||||
**Ready** = it works deeply and correctly enough for paid work.
|
||||
|
||||
| Area | Weight | Breadth | Ready | Remaining (h) | What's there / what's missing |
|
||||
|---|---:|---:|---:|---:|---|
|
||||
| Recording and monitoring | 12 % | 75 % | **40 %** | 40–60 | Punch in/out, QuickPunch-style punch on the fly, pre/post-roll, loop record into playlists, input monitoring through the strip, autosave/recovery. Missing: input/output device choice, buffer size, duplex low-latency path, record straight to disk (#84 open), per-channel hardware input routing, TrackPunch/destructive punch beyond flags, monitoring opt-out (#42) |
|
||||
| Editing | 15 % | 92 % | **65 %** | 40–60 | Slip/Shuffle/Spot/Grid, every Edit menu command, fades/crossfades, playlists and comping, clip groups, nudge, Tab to transient, separate/heal/consolidate/strip silence. Open bugs: #3, #5 (Multitool handles), #140 (Heal shortcut), #86, #91, #92; no batch fade presets, no field-recorder workflows |
|
||||
| Mixing and routing | 12 % | 85 % | **60 %** | 35–50 | Faders, pan, sends, busses, aux, masters, VCAs, folders, 10 inserts, PDC, solo modes, I/O Setup. Missing: sidechain inputs, hardware-channel I/O mapping, bus/AFL/PFL paths verified, HEAT; bugs #59, #103 |
|
||||
| Automation | 7 % | 80 % | **55 %** | 20–30 | Lanes for volume/pan/mute/sends/plugin params, trim, Write/Touch/Latch, glide/thin/coalesce. Missing: surround-pan and object automation, Touch/Latch prime and preview modes, automation on control surfaces; bug #104 |
|
||||
| Built-in plugins and AudioSuite | 8 % | 40 % | **35 %** | 80–120 | 26 processors + 2 instruments covering every AudioSuite category; Pro Tools bundles roughly three times as many plugins plus virtual instruments. No fidelity comparison, generic parameter UIs (#139). See [plugin-parity.md](plugin-parity.md) |
|
||||
| Third-party plugin hosting | 8 % | 70 % | **45 %** | 40–60 | CLAP, VST3, AU: audio, params, latency, notes, state, presets, off-thread lifecycle, CLAP floating GUIs, VST3 editors on macOS. Missing: AU editors, VST3 editors on Windows/Linux, sidechain buses, ARA, out-of-process sandboxing, macOS library-validation entitlement (#153, #149). AAX cannot be hosted (Avid SDK licence, PACE signing) |
|
||||
| MIDI and instruments | 8 % | 70 % | **30 %** | 50–80 | Piano roll, event list, step input, quantize/transpose, real-time properties, SMF import/export. Missing: **any MIDI device input/output** (no keyboard recording, no external synths, no MIDI clock/MTC out), MIDI learn, instrument depth (Sampler PR #108 open), bug #98 |
|
||||
| Score and notation | 2 % | 60 % | **35 %** | 20–30 | Grand-staff Score Editor, MusicXML export, printable SVG. Missing: notation editing depth (tuplets, articulations, lyrics, parts layout) |
|
||||
| Elastic Audio, TCE, Beat Detective, pitch | 5 % | 70 % | **35 %** | 40–60 | Pitch-preserving warp on Elastic tracks, TCE, conform to tempo, Beat Detective and Identify Beat, audio-to-MIDI. Missing: per-algorithm choice (polyphonic/rhythmic/monophonic/varispeed/X-Form), warp markers UI depth, Elastic Pitch, quality vs Pro Tools unmeasured |
|
||||
| Surround and immersive | 5 % | 55 % | **35 %** | 50–80 | Formats stereo → 9.1.6 and Ambisonics, surround panner, fold-down, multichannel bounce, object/bed routing. Missing: Dolby Atmos renderer integration and ADM BWF, MPEG-H (new in 2026.4), binaural/headphone monitoring, surround automation; bug #89 |
|
||||
| Video and post | 5 % | 60 % | **40 %** | 40–60 | Video track, H.264/ProRes/MJPEG decoders, sync offset, relinking, timecode rulers. Missing: HEVC/DNx/AV1, video bounce/export, timecode sync to external machines, speech-to-text transcript, field-recorder/iXML conform |
|
||||
| Session, file management, interchange | 10 % | 55 % | **35 %** | 60–100 | `.scraft` + Audio Files, Save Copy In, Import Session Data, clip groups, session text export, audio-health report. Missing: `.ptx` read/write, AAF/OMF (#40), session templates, track presets, workspace/Soundbase browser, disk streaming of long sources, batch rename; bugs #86, #91, #136 |
|
||||
| Collaboration, cloud, Sketch, Splice | 2 % | 0 % | **0 %** | not estimated | Avid services; out of scope unless the owner decides otherwise |
|
||||
| Agent control (CLI, control channel, MCP) | 1 % | 100 % | **100 %** | 3 | Ahead of Pro Tools' scripting SDK; one open bug (#145) |
|
||||
| **Weighted** | 100 % | **~70 %** | **~45 %** | **~520–810** | |
|
||||
|
||||
## Stage
|
||||
|
||||
**Pre-alpha.** Readiness (~40 %) is at the alpha bar, but the core-workflow gate
|
||||
([`roadmap.md`](roadmap.md#alpha-gate)) fails three of six Pro Tools workflows on macOS, the main
|
||||
platform:
|
||||
|
||||
- Tracking from a chosen interface at a chosen buffer size: **no**. Only the default devices are
|
||||
used, the mix runs in fixed 512-sample blocks, input and output are separate streams, and
|
||||
takes are held in RAM.
|
||||
- MIDI recording: **no**. There is no MIDI device layer.
|
||||
- Session interchange with Pro Tools or picture editors: **no**. No AAF/OMF/`.ptx`.
|
||||
|
||||
Editing, mixing with plugins, bouncing, and saving and reopening pass (with known bugs).
|
||||
|
||||
**Distance to alpha: ~5 points of readiness and ~110–180 agent-hours** (G-01, G-02, G-03, part
|
||||
of G-06, and the #153 entitlement). **Distance to beta: ~35 points and ~350–570 agent-hours**
|
||||
(beta also needs ~75 % ready). The beta plan is in [`roadmap.md`](roadmap.md).
|
||||
|
||||
## Effort and calibration
|
||||
|
||||
Hours are **Opus 5.5 agent wall-clock hours**: one agent session working sequentially, including
|
||||
tests and verification.
|
||||
|
||||
**Calibrated against this repo's history** (`git log`, `gh pr list`):
|
||||
|
||||
- The 10-05 bulk commits (`first commit` through `Video track, Audio Unit hosting…`, ~70k lines)
|
||||
carry batch-commit times, not authoring times, so they don't calibrate.
|
||||
- The public week (2026-10-07 → 10-10, 42 merged PRs) does. Focused bug fixes with a regression
|
||||
test, +20 to +120 lines (#116–#122, #25, #28, #37, #61), took **~0.5–1.5 h** each. Small
|
||||
features of +200 to +1,000 lines (#32 light theme +516, #11 log file +981, #8 credits +666, #23
|
||||
plugin menus +259, #76 clip-gain fader +198) took **~2–5 h**: about **200–300 tested lines per
|
||||
agent-hour**.
|
||||
- Areas were sized by the code they need at that rate (for example, MIDI device I/O plus MIDI
|
||||
recording ≈ 3–5k lines ≈ 12–25 h), plus a 1.3–1.5× tail factor for verification against Pro
|
||||
Tools behaviour, as FilmCraft observed for its long tail.
|
||||
|
||||
**Parallelism.** About 60–70 % of the work parallelizes across 4–5 agents by crate (dsp plugins,
|
||||
hardware/playback, file interchange, UI, localization). The audio-thread and hardware work
|
||||
serializes more, because it needs hands-on testing.
|
||||
|
||||
**Needs a human:**
|
||||
- Owner decisions: AAX (requires Avid's SDK licence and PACE signing; probably never), `.ptx`
|
||||
reverse engineering (clean-room implications), Avid cloud features (out of scope by default).
|
||||
- Hardware we don't have in CI: multichannel interfaces, ASIO drivers, control surfaces (HUI/MCU,
|
||||
EUCON is Avid-proprietary), MIDI keyboards, sync generators.
|
||||
- Native-speaker review for every language beyond English.
|
||||
- Ears: a mix engineer's A/B of plugins, fades and elastic audio.
|
||||
|
||||
## Revision history
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-10 | minor | Readiness-by-audience table: hours to ~95 % for full (950–1,450 h), mainstream (380–600 h) and essentials (110–200 h). The full number is already the additive weighted sum over the written dimension table (40.6 %); method unchanged |
|
||||
| 2026-10-10 | minor | Added the mainstream-practitioner (~36 %) and essentials-user (~57 %) numbers with written weights, discounts and user evidence. Checked the full ~40 %: it is the written dimension weights × values (40.6 %), so it doesn't move |
|
||||
| 2026-10-10 | minor | Stage alpha → pre-alpha under the core-workflow gate; alpha distance ~110–180 h |
|
||||
| 2026-10-10 | major | Full re-measure against Pro Tools Ultimate 2026.4.1: two numbers (breadth ~70 %, ready ~40 %), dimension and area tables with hours, stage alpha; hours re-calibrated from PR history (110–150 h → 950–1,450 h to full parity: the old figure counted presence only) |
|
||||
| 2026-10-07 | major | Area table and "~65 % overall, 110–150 h" estimate (then in ROADMAP.md) |
|
||||
@@ -0,0 +1,67 @@
|
||||
# UI parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** major (first interaction checklist against Pro Tools 2026.4.1) · **Target:** Avid Pro Tools Ultimate 2026.4.1
|
||||
|
||||
How SoundCraft feels in the hands of a Pro Tools user: tools, modes, modifiers, shortcuts,
|
||||
windows, meters. Measured from `crates/model/src/session.rs` (tools, modes), the command
|
||||
registry (shortcuts), `crates/ui-egui/src` (windows, gestures) and user issues; compared with
|
||||
reference screenshots kept locally under `plan/protools/screenshots/` (16, never committed).
|
||||
|
||||
**UI/UX fidelity: ~55 % ready** (estimated). Remaining ≈ 60–100 h.
|
||||
|
||||
Legend: ✅ matches · 🟡 partial · ❌ missing.
|
||||
|
||||
## Tools and modes
|
||||
|
||||
| Feature | Status | Notes |
|
||||
|---|---|---|
|
||||
| Tools: Zoom, Trim, Selector, Grabber, Scrubber, Pencil, Smart (7) | ✅ present | Trim variants (TCE, loop, scrub trim) and Grabber variants (separation, object) partial; Multitool fade/trim handles don't adjust audio (#5) |
|
||||
| Edit modes: Shuffle, Slip, Spot, Grid (absolute/relative) | ✅ | Spot dialog present |
|
||||
| Grid and nudge values, all timebases | ✅ | Bars/Beats, Min:Secs, Timecode, Feet+Frames, Samples |
|
||||
| Keyboard focus modes (Commands, Clip List, Groups) | 🟡 | Commands focus single-key editing ✅; list focus partial |
|
||||
| Link timeline/edit, link track/edit, Tab to transient, insertion follows playback | ✅ | |
|
||||
| Mirrored MIDI, MIDI merge, layered editing | 🟡 | Stored and partly honoured |
|
||||
|
||||
## Keyboard and mouse
|
||||
|
||||
| Feature | Status | Notes |
|
||||
|---|---|---|
|
||||
| Default shortcuts | 🟡 146 bound (measured: distinct `Some("…")` shortcuts in the registry) | Pro Tools' default set is several hundred incl. numeric keypad and focus keys. Collisions: Mix/Edit toggle vs zoom (#144), Heal (#140), Export (#7) |
|
||||
| Ctrl as Cmd on Windows/Linux | ✅ since #15, #41 still open in some builds | #62 shows Ctrl in labels |
|
||||
| Numeric keypad transport and memory-location recall | 🟡 | Classic / Transport / Shuttle modes not all present |
|
||||
| Modifier gestures (Alt for all tracks, Shift-Alt for selected, Cmd-click fine faders, Ctrl-click defaults) | 🟡 | Main ones on faders and pan; not across every control |
|
||||
| Scroll/zoom: wheel, trackpad pinch | 🟡 | Anchored trackpad zoom in open PRs #112, #137 |
|
||||
| Drag and drop: Clip List → tracks, files → tracks | ✅ macOS/Windows; 🟡 Wayland (PR #150) | |
|
||||
| Context menus (clip, track name, ruler, Clip List) | 🟡 | Clip and track menus exist; depth lower than Pro Tools'; Clip List delete (#4) |
|
||||
|
||||
## Windows and panels
|
||||
|
||||
| Window | Status | Notes |
|
||||
|---|---|---|
|
||||
| Edit window: rulers, track headers, view columns, Clip List, Groups, Tracks list | ✅ | Track Pin (new in 2026.4) ✅ (`track.pin`) |
|
||||
| Mix window: strips, views (inserts, sends, I/O, comments, EQ curve, preamps, instruments, object) | ✅ | Narrow Mix ✅; Delay Compensation view and HEAT missing |
|
||||
| MIDI Editor, Score Editor, Event List | 🟡 | Dock height bug fixed (#113); notation editing shallow |
|
||||
| Transport, Big Counter, Memory Locations, Automation, Video, Universe, Renderer, UI Customization | ✅ present | |
|
||||
| Workspace / sound browser with previews | ❌ | G-16 |
|
||||
| I/O Setup | 🟡 | Busses and paths; no hardware channel matrix (G-01) |
|
||||
| Preferences | 🟡 | Many settings stored, fewer honoured |
|
||||
| Window configurations, Tile/Cascade | 🟡 | Configurations ✅; Tile/Cascade in PR #151 |
|
||||
| Plugin windows | 🟡 | Generic parameter sliders; Pro Tools plugins have bespoke GUIs (#138 asks for skeuomorphic design); multi-page plugin views (#6) |
|
||||
| Command palette (Search) | ✅ | Over every command |
|
||||
|
||||
## Look and meters
|
||||
|
||||
| Feature | Status | Notes |
|
||||
|---|---|---|
|
||||
| Dark theme close to Pro Tools' | ✅ | Light theme (#32) and live system appearance (#85) beyond Pro Tools |
|
||||
| Interface scaling | 🟡 | Slider in PR #147 |
|
||||
| Meter types and ballistics (peak, RMS, VU, K-scales, gain reduction) | 🟡 | Peak/RMS; meter type choice and GR meters partial |
|
||||
| Clip indicators, track colours, waveform views (peak/power/rectified/outlines) | ✅ | |
|
||||
| Fader resolution, fine adjust, numeric entry | 🟡 | |
|
||||
| macOS system menu bar | ✅ | #10 |
|
||||
|
||||
## Revision history
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-10 | major | Created: tools, modes, shortcuts, windows and meters checklist against Pro Tools 2026.4.1 |
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build and package SoundCraft for Linux (<arch> is x86_64 or aarch64):
|
||||
# Build and package SoundCraft for Linux (<arch> is x86_64 or aarch64, or riscv64 when cross-compiling):
|
||||
#
|
||||
# $DIST/soundcraft-<version>-linux-<arch>.AppImage any distro with glibc >= the build host's
|
||||
# $DIST/soundcraft-<version>-linux-<arch>.AppImage.zsync delta updates (needs zsyncmake)
|
||||
@@ -13,6 +13,10 @@
|
||||
# (downloaded into $CARGO_TARGET_DIR if missing). Build on an old distro (CI: Ubuntu 22.04,
|
||||
# glibc 2.35) so the binaries run on newer ones. Optional: desktop-file-validate, appstreamcli,
|
||||
# zsyncmake (the zsync package) for the AppImage's .zsync.
|
||||
#
|
||||
# Cross-compiling (CI's linux-riscv64 job, tar.gz only): set CROSS_ARCH (riscv64), CROSS_TARGET
|
||||
# (the Rust target), CROSS_COMPILE (the binutils prefix, for strip) and optionally EMULATOR
|
||||
# (e.g. qemu-riscv64) to smoke-test the CLI.
|
||||
set -euo pipefail
|
||||
# shellcheck source=../env.sh
|
||||
. "$(dirname "${BASH_SOURCE[0]}")/../env.sh"
|
||||
@@ -25,15 +29,16 @@ while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--skip-build) SKIP_BUILD=1; shift ;;
|
||||
--formats) FORMATS="$2"; shift 2 ;;
|
||||
-h | --help) sed -n '2,17p' "$0"; exit 0 ;;
|
||||
-h | --help) sed -n '2,19p' "$0"; exit 0 ;;
|
||||
*) echo "unknown argument: $1" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
ARCH="$(uname -m)"
|
||||
ARCH="${CROSS_ARCH:-$(uname -m)}"
|
||||
case "$ARCH" in
|
||||
x86_64) DEB_ARCH=amd64 ;;
|
||||
aarch64 | arm64) ARCH=aarch64; DEB_ARCH=arm64 ;;
|
||||
riscv64) DEB_ARCH=riscv64 ;;
|
||||
*) echo "unsupported architecture $ARCH" >&2; exit 2 ;;
|
||||
esac
|
||||
export SOUNDCRAFT_MAINTAINER="${SOUNDCRAFT_MAINTAINER:-SoundCraft maintainers <soundcraft@storyteller.ai>}"
|
||||
@@ -42,9 +47,13 @@ BASENAME="soundcraft-$VERSION-linux-$ARCH"
|
||||
echo "==> SoundCraft $VERSION for Linux $ARCH ($FORMATS)"
|
||||
|
||||
if [ "$SKIP_BUILD" = 0 ]; then
|
||||
(cd "$ROOT" && cargo build --release --locked -p soundcraft -p soundcraft-cli)
|
||||
if [ -n "${CROSS_TARGET:-}" ]; then
|
||||
(cd "$ROOT" && cargo build --release --locked -p soundcraft -p soundcraft-cli --target "$CROSS_TARGET")
|
||||
else
|
||||
(cd "$ROOT" && cargo build --release --locked -p soundcraft -p soundcraft-cli)
|
||||
fi
|
||||
fi
|
||||
BIN="$CARGO_TARGET_DIR/release"
|
||||
BIN="$CARGO_TARGET_DIR/${CROSS_TARGET:+$CROSS_TARGET/}release"
|
||||
WORK="$CARGO_TARGET_DIR/linux-package"
|
||||
STAGE="$WORK/root"
|
||||
rm -rf "$WORK"
|
||||
@@ -52,7 +61,7 @@ rm -rf "$WORK"
|
||||
# ---- stage an FHS tree (shared by every format) -------------------------------------------------
|
||||
install -Dm755 "$BIN/soundcraft" "$STAGE/usr/bin/soundcraft"
|
||||
install -Dm755 "$BIN/soundcraft-cli" "$STAGE/usr/bin/soundcraft-cli"
|
||||
strip "$STAGE/usr/bin/soundcraft" "$STAGE/usr/bin/soundcraft-cli" 2>/dev/null || true
|
||||
"${CROSS_COMPILE:-}strip" "$STAGE/usr/bin/soundcraft" "$STAGE/usr/bin/soundcraft-cli" 2>/dev/null || true
|
||||
install -Dm644 "$HERE/$APP_ID.desktop" "$STAGE/usr/share/applications/$APP_ID.desktop"
|
||||
install -Dm644 "$HERE/$APP_ID.mime.xml" "$STAGE/usr/share/mime/packages/$APP_ID.xml"
|
||||
mkdir -p "$STAGE/usr/share/metainfo"
|
||||
@@ -135,6 +144,10 @@ if has appimage; then
|
||||
fi
|
||||
fi
|
||||
|
||||
"$STAGE/usr/bin/soundcraft-cli" --version
|
||||
if [ -z "${CROSS_TARGET:-}" ]; then
|
||||
"$STAGE/usr/bin/soundcraft-cli" --version
|
||||
elif [ -n "${EMULATOR:-}" ]; then
|
||||
"$EMULATOR" "$STAGE/usr/bin/soundcraft-cli" --version
|
||||
fi
|
||||
echo "==> done"
|
||||
ls -lh "$DIST"
|
||||
|
||||
+1
-1
@@ -113,7 +113,7 @@ fn assets() -> Result<(), String> {
|
||||
}
|
||||
|
||||
fn parity() -> Result<(), String> {
|
||||
cargo(&["run", "-q", "-p", "soundcraft-cli", "--", "parity", "--write", "docs/parity.md"])
|
||||
cargo(&["run", "-q", "-p", "soundcraft-cli", "--", "parity", "--write", "docs/parity-checklist.md"])
|
||||
}
|
||||
|
||||
/// Crates that must compile for the web.
|
||||
|
||||
Reference in New Issue
Block a user