Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
a05b1e7
Add a built-in Auto Pan plugin
dotCooCoo Oct 24, 2026
33c979c
List the sampler in the plugin parity Instruments row
dotCooCoo Oct 24, 2026
f0f9add
AudioSuite: Vari-Fi slows a clip down to a stop or speeds it up from one
dotCooCoo Oct 24, 2026
c5ddbba
Autosave: allow the Unix-only mut, so clippy passes on Windows
dotCooCoo Oct 24, 2026
b322ff2
CLI tests: the several-roots test uses plain paths, so it passes on W…
dotCooCoo Oct 24, 2026
40ced81
Add a built-in Subharmonic plugin
dotCooCoo Oct 24, 2026
dc9e849
Docs: the built-in plugin counts match the registry, and the sampler …
dotCooCoo Oct 24, 2026
4a0e4ca
Merge main into feat/auto-pan
dotCooCoo Oct 24, 2026
02b1537
Merge main into feat/vari-fi-speed-ramps
dotCooCoo Oct 24, 2026
9b60e84
Merge main into feat/subharmonic
dotCooCoo Oct 24, 2026
7b19c30
ROADMAP: set Last reviewed to the date of this update
dotCooCoo Oct 24, 2026
d5be1e6
Clip > Loop... opens Clip Looping: Number of Loops, Loop Length, or u…
dotCooCoo Oct 24, 2026
3e56738
Vari-Fi: hold the first and last samples at the ends of the input
dotCooCoo Oct 24, 2026
0ca492e
Strip Silence: Extract keeps the silence, and Separate cuts the clips…
dotCooCoo Oct 24, 2026
1e9a0e9
Strip Silence: a mode that is not a string is refused
dotCooCoo Oct 24, 2026
698507c
Strip Silence: Rename sets the names of the clips it leaves
dotCooCoo Oct 24, 2026
a41f254
Fades: Create refuses a shape that is not a string over a selection
dotCooCoo Oct 24, 2026
fe4cd80
Clip List: Auto Rename sets the names of the clips separated from a clip
dotCooCoo Oct 24, 2026
746a825
Strip Silence: rectangles show what Strip keeps while the window is open
dotCooCoo Oct 25, 2026
06f3062
Clip List: a paste reuses a held Auto Rename scheme only when its tex…
dotCooCoo Oct 25, 2026
c0dc2c3
Merge PR #1017: Autosave: allow the Unix-only mut, so clippy passes o…
aiongg Oct 25, 2026
10e26f6
Merge PR #1018: CLI tests: the several-roots test uses plain paths, s…
aiongg Oct 25, 2026
a6cec46
Merge PR #1010: Add a built-in Ring Modulator plugin
aiongg Oct 25, 2026
f77c360
Merge PR #1019: Add a built-in Subharmonic plugin
aiongg Oct 25, 2026
be75be8
Merge PR #1013: Add a built-in Auto Pan plugin
aiongg Oct 25, 2026
092122b
Merge PR #1016: AudioSuite: Vari-Fi slows a clip down to a stop or sp…
aiongg Oct 25, 2026
3a139d4
Integration: AudioSuite speed ramp: name SoundCraft's ids speed_ramp
aiongg Oct 25, 2026
a8ca533
Merge PR #1053: Fades: Create refuses a shape that is not a string ov…
aiongg Oct 25, 2026
4311742
Merge PR #1030: Clip > Loop... opens Clip Looping: Number of Loops, L…
aiongg Oct 25, 2026
54467f5
Merge PR #1040: Strip Silence: Extract keeps the silence, and Separat…
aiongg Oct 25, 2026
e910e72
Merge PR #1044: Strip Silence: Rename sets the names of the clips it …
aiongg Oct 25, 2026
835de9f
Integration: Strip Silence naming: refuse a name and suffix too long …
aiongg Oct 25, 2026
e463396
Merge PR #1055: Strip Silence: rectangles show what Strip keeps while…
aiongg Oct 25, 2026
9821168
Merge PR #1054: Clip List: Auto Rename sets the names of the clips se…
aiongg Oct 25, 2026
bbebfa1
Merge PR #1023: Docs: the built-in plugin counts match the registry, …
aiongg Oct 25, 2026
e954639
Integration: docs: built-in plugin counts are 27 processors + 3 instr…
aiongg Oct 25, 2026
9048e04
Integration: AudioSuite speed ramp: the clip suffix is speeramp
aiongg Oct 25, 2026
d7b1562
Integration: Strip Silence naming: rustfmt
aiongg Oct 25, 2026
35905fb
Integration: Auto Rename: a loaded scheme leaves room for its number
aiongg Oct 25, 2026
353ea14
Integration: AudioSuite speed ramp: Fit To plays the first two thirds…
aiongg Oct 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,11 +122,11 @@ where things are.
- **Automation:** volume, pan, mute, send and every plugin parameter; Write, Touch, Latch and
Trim passes recorded live from the faders; thin, glide, coalesce and convert to clip gain.
- **Plugins:** 7-band and 1-band EQ, compressor/limiter, expander/gate, de-esser, maximizer,
channel strip, room and plate reverbs, mod delay, chorus, flanger, phaser, ring modulator,
saturator, lo-fi, rectifier, pitch shifter, time shift, gain, trim, invert, DC removal,
signal generator, dither, a subtractive synth, a drum synth and a sampler (plays any WAV,
AIFF, FLAC, MP3 or OGG across the keyboard), all original. **CLAP plugins** load too. Any
processor runs offline as AudioSuite, and user presets save per plugin.
channel strip, room and plate reverbs, mod delay, auto pan, chorus, flanger, phaser, ring
modulator, saturator, lo-fi, rectifier, subharmonic, pitch shifter, time shift, gain, trim,
invert, DC removal, signal generator, dither, a subtractive synth, a drum synth and a sampler
(plays any WAV, AIFF, FLAC, MP3 or OGG across the keyboard), all original. **CLAP plugins**
load too. Any processor runs offline as AudioSuite, and user presets save per plugin.
- **MIDI:** instrument and MIDI tracks, a piano-roll MIDI editor with a velocity lane, an event
list, a score view, step input, quantize, transpose, real-time properties, Standard MIDI Files,
and Audio-to-MIDI from a sung or played melody.
Expand Down
18 changes: 17 additions & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

**Stage: pre-alpha** · next: alpha, ~5% and ~110–180 h away (the alpha gate's three failing workflows)

> **Last reviewed:** 2026-10-19 · **Last updated:** 2026-10-23 · **Change:** trivial (Ring Modulator plugin, Float32 file headroom) · **Target:** Avid Pro Tools Ultimate 2026.4.1
> **Last reviewed:** 2026-10-24 · **Last updated:** 2026-10-24 · **Change:** trivial (Clip List Auto Rename, Strip Silence rectangles, Strip Silence Rename, Strip Silence Extract and Separate, Clip Looping dialog, AudioSuite Vari-Fi speed ramps, Auto Pan plugin, Subharmonic plugin, Ring Modulator plugin, Float32 file headroom) · **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 page is
Expand Down Expand Up @@ -122,6 +122,14 @@ mutated and truncated movies. Field reports from the first public week are track

## Progress log

- **2026-10-24:** Clip List › Auto Rename (`clip.auto_rename`) sets the names that editing gives the clips it separates from the selected clips: a name, a starting number, a number of places (zeros before the number) and a suffix. The piece after each cut takes the next name, the piece before it keeps its name, and all the pieces count up one number sequence. Pieces that Strip Silence names with its own Rename fields, or removes, take no Auto Rename name. Copied clips keep their scheme when they are pasted, saved sessions hold the schemes, and Import Session Data brings them along. No readiness percentage change.
- **2026-10-24:** While the Strip Silence window is open, the Edit window draws a rectangle around each range that Strip would keep on the selected tracks, padded by Clip Start Pad and Clip End Pad, and the rectangles follow the sliders as they move. They are removed when the window closes. `edit.strip_silence_preview` returns the same ranges without changing the session. No readiness percentage change.
- **2026-10-24:** The Strip Silence window has a Rename button. It opens Rename Selected Clips, where a name, a starting number, a count of zeros and a suffix set the names of the clips that Strip, Extract and Separate leave in the selection (for example SFX023.Reel1, SFX024.Reel1). The app keeps the scheme for later uses of Strip Silence until the app quits, or until Clear and then OK empty it. No readiness percentage change.
- **2026-10-24:** The Strip Silence window has Extract and Separate buttons. Extract removes the audio and keeps the silent parts of the selection, and Separate separates the clips at the edges of the silence without removing anything. Minimum Strip Duration goes up to 4,000 ms. No readiness percentage change.
- **2026-10-24:** Clip › Loop… (Cmd+Alt+L) opens the Clip Looping dialog: Number of Loops, Loop Length in the main counter's time format, or Loop Until End of Session or Next Clip. A loop length that is not a whole number of loops cuts the last loop short, and Unloop removes that short loop too. No readiness percentage change.
- **2026-10-24:** AudioSuite ▸ Pitch Shift ▸ Vari-Fi slows the selected clips down to a complete stop or speeds them up from one, and the pitch follows the speed. Fit To keeps each clip's length and plays two-thirds of it; Extend plays all of it and makes the clip 150 % long. Fades On fades the audio out with the slowdown or in with the speedup. Before, the menu item resampled the clip at a constant speed. No readiness percentage change.
- **2026-10-24:** A built-in Auto Pan plugin pans the mono sum of a stereo track between left and right. An LFO with seven waveforms moves it at a rate in BPM or at the session tempo, running free, once, in step with the transport, or when the level crosses a threshold or a manual trigger changes; an envelope follower can move it by level instead. Width, Manual and Path set the field, and Output sets the level. No readiness percentage change.
- **2026-10-24:** A built-in Subharmonic (`subharmonic`) adds the octave below a lower and an upper band of the input, chosen from four frequency ranges, with a direct bass band, a gain and a solo for each band, Drive with six characters, HP and LP filters, and a Mix that keeps one path at full level. No readiness percentage change.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
- **2026-10-23:** A built-in Ring Modulator (`ring_mod`) applies ring modulation, frequency modulation or a positive or negative resonator, and an LFO, an envelope follower or sample and hold moves its frequency. AudioSuite › Modulation › Sci-Fi now runs it instead of the Flanger. No readiness percentage change.
- **2026-10-19:** Float32 WAV and AIFF-C encoding preserve finite samples above full scale. Integer PCM still clips at full scale. Tests cover whole-file and streaming encoders, non-finite samples, and bounce output with normalization on and off. No readiness percentage change.
- **2026-10-18:** Mono audio devices receive the average of the stereo channels before output clipping, retaining right-only audio. Surround mixes use the same average after their ITU stereo fold-down. Regression tests cover channel balance, polarity cancellation, clipping, centered mono tracks, each 5.1 channel and several callback sizes. No readiness percentage change.
Expand Down Expand Up @@ -166,6 +174,14 @@ mutated and truncated movies. Field reports from the first public week are track

| Date | Change | Summary |
|---|---|---|
| 2026-10-24 | trivial | Progress log: Clip List Auto Rename; readiness estimates unchanged |
| 2026-10-24 | trivial | Progress log: Strip Silence rectangles; readiness estimates unchanged |
| 2026-10-24 | trivial | Progress log: Strip Silence Rename; readiness estimates unchanged |
| 2026-10-24 | trivial | Progress log: Strip Silence Extract and Separate; readiness estimates unchanged |
| 2026-10-24 | trivial | Progress log: Clip Looping dialog; readiness estimates unchanged |
| 2026-10-24 | trivial | Progress log: AudioSuite Vari-Fi speed ramps; readiness estimates unchanged |
| 2026-10-24 | trivial | Progress log: Auto Pan plugin; readiness estimates unchanged |
| 2026-10-24 | trivial | Progress log: Subharmonic plugin; readiness estimates unchanged |
| 2026-10-23 | trivial | Progress log: Ring Modulator plugin; readiness estimates unchanged |
| 2026-10-19 | trivial | Float32 file encoding preserves finite headroom (#791); readiness estimates unchanged |
| 2026-10-18 | trivial | Mono device playback retains both stereo sides and surround fold-down channels; readiness estimates unchanged |
Expand Down
17 changes: 15 additions & 2 deletions apps/soundcraft-cli/tests/it/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,20 @@ fn tmpdir(tag: &str) -> PathBuf {
dir
}

/// `path` made absolute with links followed (`/var` becomes `/private/var` on macOS), as the server
/// prints a root: without the `\\?\` prefix that `canonicalize` adds on Windows.
fn resolved(path: &Path) -> PathBuf {
let path = std::fs::canonicalize(path).unwrap();
if !cfg!(windows) {
return path;
}
let text = path.to_str().unwrap();
if let Some(rest) = text.strip_prefix(r"\\?\UNC\") {
return PathBuf::from(format!(r"\\{rest}"));
}
text.strip_prefix(r"\\?\").map_or(path.clone(), PathBuf::from)
}

/// Send `calls` (tool name, arguments) to `soundcraft-cli mcp <args>`; returns whether it exited
/// cleanly, its stderr, and the stdout lines after `initialize`, one per call.
fn mcp_session(args: &[&str], calls: &[(&str, Value)]) -> (bool, String, Vec<Value>) {
Expand Down Expand Up @@ -114,8 +128,7 @@ fn mcp_automation_roots_confine_the_server() {
/// the two roots' common parent, `..` out of a root and a link out of one stay refused.
#[test]
fn mcp_automation_roots_take_several_folders() {
// Resolved, as the server shows the roots (`/var` is `/private/var` on macOS).
let base = std::fs::canonicalize(tmpdir("several")).unwrap();
let base = resolved(&tmpdir("several"));
for d in ["in", "in2", "out", "out2", "outside"] {
std::fs::create_dir_all(base.join(d)).unwrap();
}
Expand Down
72 changes: 72 additions & 0 deletions crates/dsp/src/offline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,78 @@ pub fn pitch_shift(ch: &[Vec<f32>], semitones: f32, sample_rate: f32) -> Vec<Vec
out
}

/// Which way [`speed_ramp`] ramps the playback speed.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SpeedRampChange {
/// From normal speed down to a complete stop.
SlowDown,
/// From a complete stop up to normal speed.
SpeedUp,
}

/// How much of the input [`speed_ramp`] plays, and how long the result is.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SpeedRampSelection {
/// The result is as long as the input. Two thirds of the input are played: the first two
/// thirds for Slow Down, the last two thirds for Speed Up, so each joins its neighbour
/// without a jump.
FitTo,
/// All of the input is played, and the result is 150 % of its length.
Extend,
}

/// Plays `ch` with the speed ramping between normal speed and a complete stop, so the pitch follows
/// the speed. The speed is `1 - u²` for Slow Down and `1 - (1 - u)²` for Speed Up, where `u`
/// runs from 0 to 1 over the result; both curves average 2/3 of normal speed. With `fades`, the
/// gain follows the same curve: a fade-out for Slow Down and a fade-in for Speed Up. Every channel
/// is read at the same positions, and a channel shorter than the others is silent past its end.
pub fn speed_ramp(ch: &[Vec<f32>], change: SpeedRampChange, selection: SpeedRampSelection, fades: bool) -> Vec<Vec<f32>> {
let len = max_len(ch) as f64;
let (total, first, played) = match selection {
SpeedRampSelection::Extend => ((len * 1.5).round(), 0.0, len),
SpeedRampSelection::FitTo => {
let played = len * 2.0 / 3.0;
(len, if change == SpeedRampChange::SlowDown { 0.0 } else { len - played }, played)
}
};
let out_len = (total as usize).min(MAX_OFFLINE_SAMPLES);
let table = SincTable::new(0.97);
let hw = table.half_width;
// The fraction of the played input reached at `u`, and the speed there (0 to 1).
let at = |u: f64| -> (f64, f64) {
match change {
SpeedRampChange::SlowDown => ((3.0 * u - u * u * u) / 2.0, 1.0 - u * u),
SpeedRampChange::SpeedUp => {
let r = 1.0 - u;
(1.0 - (3.0 * r - r * r * r) / 2.0, 1.0 - r * r)
}
}
};
// The interpolation reads the first or last sample of the input for taps before or after it.
let last = (len as i64 - 1).max(0);
ch.iter()
.map(|x| {
(0..out_len)
.map(|n| {
let (pos, speed) = at(n as f64 / total);
let t = first + played * pos;
let lo = (t - hw).ceil() as i64;
let hi = (t + hw).floor() as i64;
let mut acc = 0.0f32;
let mut j = lo;
while j <= hi {
if let Some(&v) = x.get(j.clamp(0, last) as usize) {
acc += v * table.at(t - j as f64);
}
j += 1;
}
if fades { acc * speed as f32 } else { acc }
})
.collect()
})
.collect()
}

// ---------------------------------------------------------------------------------------------
// Generators and analysis

Expand Down
Loading
Loading