Browse / github.com/storytold/soundcraft / patch
Keep unity-gain clips unchanged when coalescing clip gain to volume
Keep unity-gain clips unchanged when coalescing clip gain to volume Found while testing `crates/engine/src/cmd/edit_more.rs:825-845 (coalesce_clip_gain_to_volume)` (no existing issue): Coalesce Clip Gain to Volume raises the level of unity-gain clips that sit before a boosted clip. ## Cause `coalesce_clip_gain_to_volume` built the volume-lane breakpoints only from clips with non-zero gain or a gain envelope. `AutomationLane::value_at` holds the first and last point values outside the breakpoints, so unity-gain clips before the first or after the last boosted clip played at the boosted clip's level. ## Fix Breakpoints are now added at the start and end of eve
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- Silvertree2010 (imported from pull request #770, open)
- Base
2e9ad2f4215bd36e7edf30579e8ce520d5ce67c7- Size
- +25 −3 in 1 file
- Review
- pending
- Shared
- 2026-10-28
- Id
9597141433ab8797be3b2ed0
How it went
No other operator has reported using it yet.
Diff
raw · crates/engine/src/cmd/edit_more.rs
@@ -822,21 +822,24 @@ fn coalesce_clip_gain_to_volume(e: &mut Engine, p: &Value) -> Result<Value> { let mut n = 0; for t in &tracks { let Some(tr) = s.track_mut(*t) else { continue };- let clips: Vec<Clip> = tr.clips().iter().filter(|c| c.is_audio() && (c.gain_db != 0.0 || !c.gain_env.is_empty())).cloned().collect();+ // Every audio clip anchors the lane at its own level, so unity clips outside the+ // boosted ones don't inherit the value the lane holds before/after its end points.+ let audio: Vec<Clip> = tr.clips().iter().filter(|c| c.is_audio()).cloned().collect();+ let clips: Vec<Clip> = audio.iter().filter(|c| c.gain_db != 0.0 || !c.gain_env.is_empty()).cloned().collect(); if clips.is_empty() { continue; } let lane = tr.lane(&AutoParam::Volume).cloned(); let fader = tr.mixer.volume_db; let mut times: Vec<Samples> = lane.as_ref().map(|l| l.points.iter().map(|pt| pt.at).collect()).unwrap_or_default();- for c in &clips {+ for c in &audio { times.push(c.start); times.push(c.end() - 1); times.extend(c.gain_env.iter().map(|x| c.start + x.0)); } times.sort_unstable(); times.dedup();- let gain_at = |at: Samples| clips.iter().find(|c| c.range().contains(at)).map_or(0.0, |c| clip_db(c, at - c.start));+ let gain_at = |at: Samples| audio.iter().find(|c| c.range().contains(at)).map_or(0.0, |c| clip_db(c, at - c.start)); let pts: Vec<(Samples, f32)> = times.iter().map(|&at| (at, volume_at(lane.as_ref(), fader, at) + gain_at(at))).collect(); let l = tr.lane_mut(&AutoParam::Volume); l.points.clear();@@ -1101,6 +1104,25 @@ mod tests { assert_eq!(tr.clips()[0].gain_db, 0.0); } + #[test]+ fn coalesce_clip_gain_keeps_unity_clips_unchanged() {+ let (mut e, t, ids) = session_with_clips(&[(0, 4096), (8192, 4096), (16384, 4096)]);+ let boosted = ids[1];+ let pl = e.session_mut().track_mut(t).and_then(|tr| tr.playlist_mut()).expect("playlist");+ for c in pl.clips.iter_mut().filter(|c| c.id == boosted) {+ c.gain_db = 6.0;+ }+ e.execute("edit.automation_coalesce_clip_gain_to_volume", &json!({})).unwrap();+ let tr = e.session().track(t).unwrap();+ let lane = tr.lane(&AutoParam::Volume);+ let fader = tr.mixer.volume_db;+ for at in [0, 1000, 4095, 16384, 18000, 20479] {+ let v = volume_at(lane, fader, at);+ assert!((v - fader).abs() < 1e-3, "unity clip at {at} changed level: {v} (fader {fader})");+ }+ assert!((volume_at(lane, fader, 10_000) - fader - 6.0).abs() < 1e-3);+ }+ #[test] fn glide_and_trim() { let (mut e, t, _) = session_with_clips(&[(0, 1000)]);