Browse / github.com/storytold/soundcraft / patch
Strip Silence: the sliders move a tenth as far while Ctrl or Command is held
Strip Silence: the sliders move a tenth as far while Ctrl or Command is held On `main`, a drag on a Strip Silence slider always moves the value as far as the pointer, so the threshold moves about 1.1 dB for each point. The reference guide (Pro Tools Reference Guide 2020.12, Chapter 41, page 926, step 4) says of Strip Threshold and Minimum Strip Duration: "For finer resolution on these controls, press Command (Mac) or Control (Windows) while adjusting them." Chapter 3's shortcut table gives the same modifier for "Fine adjustment of sliders, knobs, and breakpoints", which covers Clip Start Pad and Clip End Pad. The guide is at https://web.archive.org/web/20210805202452/h
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- dotCooCoo (imported from pull request #1056, open)
- Base
cf96cb888cdbeed5be9c79c6ab54e63d9247f347- Size
- +210 −7 in 6 files
- Review
- pending
- Shared
- 2026-10-28
- Id
9af3b7a17f4997192791d175
How it went
No other operator has reported using it yet.
Diff
raw · ROADMAP.md crates/ui-egui/src/dialogs.rs crates/ui-egui/src/widgets.rs crates/ui-egui/tests/it/main.rs crates/ui-egui/tests/it/strip_silence_sliders.rs docs/ui-parity.md
@@ -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-24 · **Last updated:** 2026-10-25 · **Change:** trivial (Auto Pan stereo balance, 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+> **Last reviewed:** 2026-10-24 · **Last updated:** 2026-10-25 · **Change:** trivial (Auto Pan stereo balance, Strip Silence fine adjustment, 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@@ -123,6 +123,7 @@ mutated and truncated movies. Field reports from the first public week are track ## Progress log - **2026-10-25:** Auto Pan moves the balance of a stereo track: the left channel's level falls as the position moves right and the right channel's as it moves left, at constant power, and each channel stays on its own side. At the center the track passes unchanged, so a wide or out-of-phase stereo image keeps its width, and the envelope follower follows the louder channel. No readiness percentage change.+- **2026-10-24:** The Strip Silence window's four sliders move a tenth as far for the same drag while Ctrl (Command on a Mac) is held, on the slider and on its value box. Pressing or releasing the key during a drag leaves the value where it is. The Threshold and Clip Start Pad boxes show one decimal place. No readiness percentage change. - **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.@@ -176,6 +177,7 @@ mutated and truncated movies. Field reports from the first public week are track | Date | Change | Summary | |---|---|---| | 2026-10-25 | trivial | Progress log: Auto Pan stereo balance; readiness estimates unchanged |+| 2026-10-24 | trivial | Progress log: Strip Silence fine adjustment; readiness estimates unchanged | | 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 |@@ -656,10 +656,10 @@ pub fn show(app: &mut SoundApp, ctx: &egui::Context) { } Dialog::StripSilence { threshold, min_ms, pre_ms, post_ms } => { let before = [*threshold, *min_ms, *pre_ms, *post_ms];- ui.add(egui::Slider::new(threshold, -96.0..=0.0).text("Threshold dB"));- ui.add(egui::Slider::new(min_ms, 0.0..=4000.0).text("Min strip duration ms"));- ui.add(egui::Slider::new(pre_ms, 0.0..=500.0).text("Clip start pad ms"));- ui.add(egui::Slider::new(post_ms, 0.0..=2000.0).text("Clip end pad ms"));+ crate::widgets::fine_slider(ui, threshold, -96.0..=0.0, "Threshold dB");+ crate::widgets::fine_slider(ui, min_ms, 0.0..=4000.0, "Min strip duration ms");+ crate::widgets::fine_slider(ui, pre_ms, 0.0..=500.0, "Clip start pad ms");+ crate::widgets::fine_slider(ui, post_ms, 0.0..=2000.0, "Clip end pad ms"); let values = [*threshold, *min_ms, *pre_ms, *post_ms]; // The Edit window is drawn before this window, so it draws the rectangles for new values in the next frame. if values != before {@@ -424,6 +424,76 @@ pub fn db_text(db: f32) -> String { if db <= -143.9 { "-inf".into() } else { format!("{db:.1}") } } +/// Where a [`fine_slider`] drag is anchored: the pointer's x and the value there, and the pass+/// that last drew the slider during the drag.+#[derive(Clone, Copy)]+struct FineDrag {+ x: f32,+ value: f64,+ fine: bool,+ relative: bool,+ pass: u64,+}++/// A horizontal slider that moves a tenth as far for the same drag while Ctrl (Command on a Mac)+/// is held, as a [`fader`] does, on the rail and on the value box. Pressing or releasing the key+/// during a drag on the rail leaves the value where it is, and the drag goes on from there. The+/// value box shows the decimal places that a tenth of a point's step needs, and a drag on the+/// rail that the key has touched rounds the value to them.+pub fn fine_slider(ui: &mut Ui, value: &mut f64, range: std::ops::RangeInclusive<f64>, text: &str) -> Response {+ let id = ui.scope_id().with(("fine_slider", text));+ let (fine, x) = ui.input(|i| (i.modifiers.command || i.modifiers.ctrl, i.pointer.interact_pos().map(|p| p.x)));+ // A drag lasts through the pass that releases the button, where egui still sets the value+ // from the pointer. An anchor that the previous pass did not store was left by an earlier+ // drag, and is ignored.+ let pass = ui.ctx().cumulative_pass_nr();+ let mut drag = ui.data(|d| d.get_temp::<FineDrag>(id)).filter(|d| d.pass + 1 == pass);+ let (lo, hi) = (*range.start(), *range.end());+ // The handle's travel along the rail, as `egui::Slider` lays it out.+ let thickness = ui.text_style_height(&egui::TextStyle::Body).max(ui.spacing().interact_size.y);+ let travel = (ui.spacing().slider_width - 2.0 * thickness / 2.5).max(1.0);+ let per_point = (hi - lo) / f64::from(travel);+ let scale = if fine { 0.1 } else { 1.0 };+ let decimals = (-(per_point * 0.1).log10()).ceil().clamp(0.0, 3.0) as usize;+ if let (Some(d), Some(x)) = (&mut drag, x) {+ if d.fine != fine {+ *d = FineDrag { x, value: *value, fine, relative: true, pass };+ } else if d.relative {+ let moved = d.value + f64::from(x - d.x) * per_point * scale;+ *value = egui::emath::round_to_decimals(moved, decimals).clamp(lo, hi);+ }+ }+ let hold = drag.is_some_and(|d| d.relative);+ let mut slider = egui::Slider::from_get_set(range, |v| {+ if let Some(v) = v+ && !hold+ {+ *value = v;+ }+ *value+ })+ .min_decimals(decimals)+ .text(text);+ if fine {+ slider = slider.drag_value_speed(per_point * scale);+ }+ let resp = ui.add(slider);+ let rail = Rect::from_min_size(resp.rect.min, vec2(ui.spacing().slider_width, resp.rect.height()));+ let on_rail = resp.dragged() && ui.input(|i| i.pointer.press_origin()).is_some_and(|p| rail.contains(p));+ let next = match (on_rail, drag, x) {+ (true, Some(d), _) => Some(FineDrag { pass, ..d }),+ (true, None, Some(x)) => Some(FineDrag { x, value: *value, fine, relative: fine, pass }),+ _ => None,+ };+ ui.data_mut(|m| match next {+ Some(d) => {+ m.insert_temp(id, d);+ }+ None => m.remove::<FineDrag>(id),+ });+ resp+}+ /// A speaker dot on the surround panner: label, square position (-1..1, y up = front), height layer. pub struct PannerSpeaker { pub label: &'static str,@@ -50,6 +50,7 @@ mod side_lists_text; mod strip_silence_dialog; mod strip_silence_rectangles; mod strip_silence_rename;+mod strip_silence_sliders; mod toolbar_scroll; mod toolbar_tempo; mod track_height;new file mode 100644@@ -0,0 +1,129 @@+use egui::accesskit::Role;+use egui::{Event, Modifiers, PointerButton, Pos2, vec2};+use egui_kittest::{Harness, kittest::Queryable};+use serde_json::json;+use soundcraft_ui_egui::dialogs::Dialog;+use soundcraft_ui_egui::{Services, SoundApp, menus};++const SLIDERS: [&str; 4] = ["Threshold dB", "Min strip duration ms", "Clip start pad ms", "Clip end pad ms"];++/// Selects the whole Kick track and opens Edit > Strip Silence from the menu.+fn strip_silence_dialog() -> Harness<'static, SoundApp> {+ let mut app = SoundApp::new(soundcraft_engine::demo::demo_engine(), None, Services::default());+ let track = app.engine.session().track_by_name("Kick").unwrap().id;+ let end = app.engine.session().content_end();+ app.run("edit.select", json!({"tracks": [track.0], "start": 0, "end": end})).unwrap();+ menus::invoke_menu(&mut app, "edit.strip_silence", "Edit > Strip Silence");+ let mut h = Harness::builder().with_size(vec2(1600.0, 1000.0)).build_ui_state(+ |ui, app: &mut SoundApp| {+ let ctx = ui.ctx().clone();+ app.logic(&ctx);+ app.ui(ui);+ },+ app,+ );+ h.run_steps(3);+ h+}++/// The value of the slider labeled `label`, in the order the window shows them.+fn value(h: &Harness<'_, SoundApp>, label: &str) -> f64 {+ let Some(Dialog::StripSilence { threshold, min_ms, pre_ms, post_ms }) = h.state().dialogs.open else { panic!("Strip Silence is not open") };+ let i = SLIDERS.iter().position(|s| *s == label).unwrap();+ [threshold, min_ms, pre_ms, post_ms][i]+}++fn hold(h: &mut Harness<'_, SoundApp>, modifiers: Modifiers) {+ h.event(Event::ModifiersChanged(modifiers));+ h.run_steps(1);+}++fn press(h: &mut Harness<'_, SoundApp>, pos: Pos2, pressed: bool, modifiers: Modifiers) {+ h.event(Event::PointerButton { pos, button: PointerButton::Primary, pressed, modifiers });+ h.run_steps(1);+}++fn move_to(h: &mut Harness<'_, SoundApp>, pos: Pos2) {+ h.event(Event::PointerMoved(pos));+ h.run_steps(1);+}++/// Presses the middle of a slider's rail (`role` `Slider`) or value box (`SpinButton`) and drags+/// `dx` points right in six steps, with `modifiers` held; returns how far the value moved from+/// where the press put it.+fn drag_on(h: &mut Harness<'_, SoundApp>, role: Role, label: &str, dx: f32, modifiers: Modifiers) -> f64 {+ hold(h, modifiers);+ let from = h.get_by_role_and_label(role, label).rect().center();+ move_to(h, from);+ press(h, from, true, modifiers);+ let start = value(h, label);+ for i in 1..=6 {+ move_to(h, from + vec2(dx * i as f32 / 6.0, 0.0));+ }+ let moved = value(h, label) - start;+ press(h, from + vec2(dx, 0.0), false, modifiers);+ assert_eq!(value(h, label) - start, moved, "{label}: releasing the button moved the value");+ hold(h, Modifiers::NONE);+ moved+}++fn drag(h: &mut Harness<'_, SoundApp>, label: &str, dx: f32, modifiers: Modifiers) -> f64 {+ drag_on(h, Role::Slider, label, dx, modifiers)+}++#[test]+fn strip_silence_sliders_move_a_tenth_as_far_with_ctrl_held() {+ let mut h = strip_silence_dialog();+ for label in SLIDERS {+ let plain = drag(&mut h, label, 30.0, Modifiers::NONE);+ let fine = drag(&mut h, label, 30.0, Modifiers::COMMAND);+ assert!(plain > 0.0, "{label}: a plain drag did not move the value");+ let ratio = fine / plain;+ assert!((0.08..=0.12).contains(&ratio), "{label}: Ctrl-drag moved {fine}, a plain drag {plain}");+ }+}++#[test]+fn ctrl_dragging_the_value_box_moves_a_tenth_as_far_too() {+ let mut h = strip_silence_dialog();+ for label in ["Threshold dB", "Clip start pad ms"] {+ let plain = drag_on(&mut h, Role::SpinButton, label, 20.0, Modifiers::NONE);+ let fine = drag_on(&mut h, Role::SpinButton, label, 20.0, Modifiers::COMMAND);+ assert!(plain > 0.0, "{label}: a plain drag on the value box did not move the value");+ let ratio = fine / plain;+ assert!((0.07..=0.13).contains(&ratio), "{label}: Ctrl-drag on the value box moved {fine}, a plain drag {plain}");+ }+}++#[test]+fn the_value_box_shows_the_tenths_a_ctrl_drag_reaches() {+ let mut h = strip_silence_dialog();+ drag(&mut h, "Threshold dB", 7.0, Modifiers::COMMAND);+ let v = value(&h, "Threshold dB");+ let shown = h.get_by_role_and_label(Role::SpinButton, "Threshold dB").value().unwrap_or_default();+ assert_eq!(shown, format!("{v:.1}"));+}++#[test]+fn pressing_and_releasing_ctrl_during_a_drag_keeps_the_value() {+ let mut h = strip_silence_dialog();+ let label = "Threshold dB";+ let plain = drag(&mut h, label, 30.0, Modifiers::NONE);+ let from = h.get_by_role_and_label(Role::Slider, label).rect().center();+ move_to(&mut h, from);+ press(&mut h, from, true, Modifiers::NONE);+ move_to(&mut h, from + vec2(10.0, 0.0));+ let before = value(&h, label);+ hold(&mut h, Modifiers::COMMAND);+ assert_eq!(value(&h, label), before, "pressing Ctrl moved the value");+ for i in 1..=6 {+ move_to(&mut h, from + vec2(10.0 + 5.0 * i as f32, 0.0));+ }+ let fine = value(&h, label) - before;+ assert!((0.08..=0.12).contains(&(fine / plain)), "Ctrl-drag moved {fine}, a plain drag {plain}");+ let held = value(&h, label);+ hold(&mut h, Modifiers::NONE);+ assert_eq!(value(&h, label), held, "releasing Ctrl moved the value back to the pointer");+ press(&mut h, from + vec2(40.0, 0.0), false, Modifiers::NONE);+ assert_eq!(value(&h, label), held, "releasing the button moved the value back to the pointer");+}@@ -1,6 +1,6 @@ # UI parity -> **Last reviewed:** 2026-10-16 · **Last updated:** 2026-10-16 · **Change:** minor (Scroll Into View in the track name menu) · **Target:** Avid Pro Tools Ultimate 2026.4.1+> **Last reviewed:** 2026-10-16 · **Last updated:** 2026-10-24 · **Change:** minor (fine adjustment on the Strip Silence sliders) · **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@@ -62,13 +62,14 @@ Legend: ✅ matches · 🟡 partial · ❌ missing. | 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 | 🟡 | |+| Fader resolution, fine adjust, numeric entry | 🟡 | Ctrl/Cmd-drag moves the faders and the Strip Silence sliders a tenth as far | | macOS system menu bar | ✅ | #10 | ## Revision history | Date | Change | Summary | |---|---|---|+| 2026-10-24 | minor | Strip Silence sliders: Ctrl/Cmd-drag moves them a tenth as far. Readiness estimate unchanged | | 2026-10-16 | minor | Track name menu: Scroll Into View brings the track to the top of the Edit window and the left of the Mix. Readiness estimate unchanged | | 2026-10-13 | minor | A plugin opens when it's inserted (#486) | | 2026-10-13 | minor | Drag across checkboxes (#399) |