Merge pull request #996 from sbacaro/fix/896-offline-render-budget

Bound offline render commands: dedupe tracks and a total sample budget (fixes #896)
This commit is contained in:
Brandon Thomas
2026-10-24 01:01:52 -04:00
committed by GitHub
2 changed files with 53 additions and 2 deletions
+46 -1
View File
@@ -140,9 +140,15 @@ fn process(e: &mut Engine, p: &Value) -> Result<Value> {
fn generate(e: &mut Engine, p: &Value, name: &str, params: &[(String, f32)], r: Range) -> Result<Value> {
let sr = e.session().sample_rate;
let tracks = tracks_param(e, "audiosuite.process", p)?;
let mut budget = soundcraft_mix::MAX_RENDER_SAMPLES;
// Refuse before generating anything, rather than cut the clip short.
for t in tracks.iter().filter_map(|t| e.session().track(*t)).filter(|t| t.kind == TrackKind::Audio) {
crate::io::refuse_longer(e.session(), "audiosuite.process", &t.name, "generate on", r.len(), crate::io::render_limit(t.channels()))?;
let channels = t.channels().max(1);
crate::io::refuse_longer(e.session(), "audiosuite.process", &t.name, "generate on", r.len(), crate::io::render_limit(channels))?;
// Each track keeps its generated buffer for the life of the command (and undo holds it), so
// cap the total samples generated across tracks, not just each one.
let frames = usize::try_from(r.len()).unwrap_or(0).min(crate::io::render_limit(channels));
budget = budget.checked_sub(frames * channels).ok_or_else(|| bad("audiosuite.process", "too much audio to generate in one command"))?;
}
let mut clips = Vec::new();
for track in tracks {
@@ -257,4 +263,43 @@ mod tests {
let ranges: Vec<_> = e.session().tracks[0].clips().iter().map(|c| (c.start, c.length)).collect();
assert_eq!(ranges, vec![(0, 32), (32, 64)]);
}
#[test]
fn duplicate_track_ids_generate_once_not_per_listing() {
let mut e = Engine::new(Session::default());
let t = e.session_mut().add_track(TrackKind::Audio, ChannelFormat::Mono, Some("Tone"));
e.execute("edit.select", &json!({"tracks": [t.0], "start": 0, "end": 4800})).unwrap();
// The same track eight times over: it must generate once, not eight times (#896).
let out = e
.execute(
"audiosuite.process",
&json!({"process": "signal_generator", "tracks": [t.0, t.0, t.0, t.0, t.0, t.0, t.0, t.0], "params": {"freq": 440.0}}),
)
.unwrap();
assert_eq!(out["processed"], 1);
assert_eq!(e.session().tracks[0].clips().len(), 1, "one clip, not one per duplicate listing");
}
#[test]
fn generate_refuses_a_total_budget_across_tracks() {
let mut e = Engine::new(Session::default());
// Two tracks each near the per-track limit: the second would push the total past the budget.
for n in 0..2 {
e.session_mut().add_track(TrackKind::Audio, ChannelFormat::Mono, Some(&format!("T{n}")));
}
let t: Vec<_> = e.session().tracks.iter().map(|t| t.id.0).collect();
// Select both tracks so signal_generator targets them.
e.execute("edit.select", &json!({"tracks": [t[0], t[1]], "start": 0, "end": 4800})).unwrap();
e.execute("audiosuite.process", &serde_json::json!({"process": "signal_generator", "track": t[0], "params": {"freq": 440.0}})).unwrap();
assert_eq!(e.session().tracks[0].clips().len(), 1, "one track generates one clip");
// Two distinct tracks over the total budget are refused (MAX_RENDER_SAMPLES is f32 samples
// across all channels; a single track over the limit is already refused per-track).
let len = (soundcraft_mix::MAX_RENDER_SAMPLES as i64) / 3 * 2;
let out = e.execute(
"audiosuite.process",
&serde_json::json!({"process": "signal_generator", "tracks": [t[0], t[1]], "params": {"freq": 440.0}, "start": 0, "end": len}),
);
let ok = matches!(out, Err(crate::EngineError::BadParams(_, ref m)) if m.contains("too long to generate") || m.contains("too much audio"));
assert!(ok, "two tracks over budget, but got: {out:?}");
}
}
+7 -1
View File
@@ -247,13 +247,19 @@ pub fn tracks_param(e: &Engine, cmd: &str, p: &Value) -> Result<Vec<TrackId>> {
if let Some(arr) = p.get("tracks").and_then(Value::as_array) {
let s = e.session();
let mut out = Vec::new();
let mut seen = std::collections::HashSet::new();
for v in arr {
let id = match (v.as_u64(), v.as_str()) {
(Some(n), _) => s.track(TrackId(n)).map(|t| t.id),
(_, Some(name)) => s.track_by_name(name).map(|t| t.id),
_ => None,
};
out.push(id.ok_or_else(|| bad(cmd, format!("unknown track {v}")))?);
let id = id.ok_or_else(|| bad(cmd, format!("unknown track {v}")))?;
// Duplicates multiply the work (each entry renders and keeps its own result), so drop
// repeated ids instead of letting them blow the budget.
if seen.insert(id) {
out.push(id);
}
}
return Ok(out);
}