From b6946d3a946485713c47d4d18dc691fd0ee9c88a Mon Sep 17 00:00:00 2001 From: Mex <44814932+MMexicann@users.noreply.github.com> Date: Wed, 4 Nov 2026 23:33:22 +0100 Subject: [PATCH 1/5] fix(engine): reject unsupported stem bounce formats Refactor file.bounce_stems command to improve format handling. --- crates/engine/src/cmd/file.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/engine/src/cmd/file.rs b/crates/engine/src/cmd/file.rs index 6cac6607..a207791e 100644 --- a/crates/engine/src/cmd/file.rs +++ b/crates/engine/src/cmd/file.rs @@ -92,9 +92,11 @@ pub fn specs() -> Vec { let dir = str_param(p, "dir").ok_or_else(|| bad("file.bounce_stems", "`dir` required"))?.to_string(); let r = range_param(e, "file.bounce_stems", p)?; let r = if r.is_empty() { soundcraft_time::Range::new(0, e.session().content_end().max(1)) } else { r }; - let format = str_param(p, "format") - .and_then(soundcraft_audio_io::FileFormat::from_write_extension) - .unwrap_or(soundcraft_audio_io::FileFormat::Wav); + let format_name = str_param(p, "format").unwrap_or("wav"); + let format = soundcraft_audio_io::FileFormat::from_write_extension(format_name).ok_or_else(|| { + let supported = soundcraft_audio_io::FileFormat::writable_extensions().join(", "); + bad("file.bounce_stems", format!("cannot write `{format_name}` files (supported: {supported})")) + })?; let bit_depth = match p.get("bit_depth").map(|b| b.to_string().trim_matches('"').to_string()).as_deref() { Some("16") => soundcraft_audio_io::BitDepth::Int16, Some("32") | Some("32f") => soundcraft_audio_io::BitDepth::Float32, From b8e1e69134686b217b2246dd9c50b0f9bd2ef610 Mon Sep 17 00:00:00 2001 From: Mex <44814932+MMexicann@users.noreply.github.com> Date: Wed, 4 Nov 2026 23:35:04 +0100 Subject: [PATCH 2/5] test(engine): cover unsupported stem output formats --- crates/engine/src/cmd/file/tests.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/crates/engine/src/cmd/file/tests.rs b/crates/engine/src/cmd/file/tests.rs index b8c5be94..018952eb 100644 --- a/crates/engine/src/cmd/file/tests.rs +++ b/crates/engine/src/cmd/file/tests.rs @@ -118,6 +118,23 @@ fn import_audio_honours_new_tracks() { let _ = std::fs::remove_dir_all(dir); } +#[test] +fn bounce_stems_rejects_unsupported_formats_before_writing() { + let dir = std::env::temp_dir().join(format!("sc-stems-unsupported-format-{}", std::process::id())); + std::fs::create_dir(&dir).unwrap(); + let stems = dir.join("stems"); + let mut e = Engine::default(); + e.execute("track.new", &json!({"count": 1, "name": "Tone", "format": "mono"})).unwrap(); + let result = e.execute("file.bounce_stems", &json!({"dir": stems, "format": "not-a-format", "start": 0, "end": 1})); + let created_output = stems.exists(); + // Own this directory exclusively; remove any baseline output before the assertion can fail. + std::fs::remove_dir_all(&dir).unwrap(); + let err = result.map(|_| ()).unwrap_err(); + assert!(err.to_string().contains("cannot write `not-a-format`"), "{err}"); + assert!(err.to_string().contains("supported: wav"), "{err}"); + assert!(!created_output, "an unsupported format must not create the output directory"); +} + #[test] fn bounce_refuses_formats_it_cannot_write() { let mut e = Engine::default(); From bf2a6b52de7a25238deba99bcadbdbaf7fd1aa64 Mon Sep 17 00:00:00 2001 From: Mex <44814932+MMexicann@users.noreply.github.com> Date: Thu, 5 Nov 2026 00:09:33 +0100 Subject: [PATCH 3/5] fix(engine): reject non-string stem output formats Address review r3017244428: only an absent format defaults to WAV; a present non-string value returns a parameter error before creating the output directory. --- crates/engine/src/cmd/file.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/engine/src/cmd/file.rs b/crates/engine/src/cmd/file.rs index a207791e..a2f5c507 100644 --- a/crates/engine/src/cmd/file.rs +++ b/crates/engine/src/cmd/file.rs @@ -92,7 +92,10 @@ pub fn specs() -> Vec { let dir = str_param(p, "dir").ok_or_else(|| bad("file.bounce_stems", "`dir` required"))?.to_string(); let r = range_param(e, "file.bounce_stems", p)?; let r = if r.is_empty() { soundcraft_time::Range::new(0, e.session().content_end().max(1)) } else { r }; - let format_name = str_param(p, "format").unwrap_or("wav"); + let format_name = match p.get("format") { + None => "wav", + Some(value) => value.as_str().ok_or_else(|| bad("file.bounce_stems", "`format` must be a string"))?, + }; let format = soundcraft_audio_io::FileFormat::from_write_extension(format_name).ok_or_else(|| { let supported = soundcraft_audio_io::FileFormat::writable_extensions().join(", "); bad("file.bounce_stems", format!("cannot write `{format_name}` files (supported: {supported})")) From b93a182ef75b9717cdd1d361d5296f5f5088c2ee Mon Sep 17 00:00:00 2001 From: Mex <44814932+MMexicann@users.noreply.github.com> Date: Thu, 5 Nov 2026 00:11:18 +0100 Subject: [PATCH 4/5] test(engine): cover non-string stem output formats Exercise null, numeric, boolean, array and object format values through Engine::execute and verify the parameter error without creating a destination. The regression fails before the guard; all 363 engine unit tests pass with it. --- crates/engine/src/cmd/file/tests.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/crates/engine/src/cmd/file/tests.rs b/crates/engine/src/cmd/file/tests.rs index 018952eb..35c821d8 100644 --- a/crates/engine/src/cmd/file/tests.rs +++ b/crates/engine/src/cmd/file/tests.rs @@ -135,6 +135,29 @@ fn bounce_stems_rejects_unsupported_formats_before_writing() { assert!(!created_output, "an unsupported format must not create the output directory"); } +#[test] +fn bounce_stems_rejects_non_string_formats_before_writing() { + let dir = std::env::temp_dir().join(format!("sc-stems-format-type-{}", std::process::id())); + std::fs::create_dir(&dir).unwrap(); + let mut e = Engine::default(); + e.execute("track.new", &json!({"count": 1, "name": "Tone", "format": "mono"})).unwrap(); + let results: Vec<_> = [json!(null), json!(123), json!(false), json!([]), json!({})] + .into_iter() + .enumerate() + .map(|(index, format)| { + let stems = dir.join(index.to_string()); + let result = e.execute("file.bounce_stems", &json!({"dir": stems, "format": format, "start": 0, "end": 1})); + (result, stems.exists()) + }) + .collect(); + std::fs::remove_dir_all(&dir).unwrap(); + for (result, created_output) in results { + let err = result.map(|_| ()).unwrap_err(); + assert!(err.to_string().contains("`format` must be a string"), "{err}"); + assert!(!created_output, "a non-string format must not create the output directory"); + } +} + #[test] fn bounce_refuses_formats_it_cannot_write() { let mut e = Engine::default(); From e1b22258bc080970bba3135f4e970023e2786d44 Mon Sep 17 00:00:00 2001 From: Mex <44814932+MMexicann@users.noreply.github.com> Date: Sun, 8 Nov 2026 13:42:03 +0100 Subject: [PATCH 5/5] test(engine): cover explicit WAV stems and reusable scratch directories Address the review by clearing stale test scratch directories before setup and exercising an explicit supported WAV format through the real command, including decoded output format, frames and audio. --- crates/engine/src/cmd/file/tests.rs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/crates/engine/src/cmd/file/tests.rs b/crates/engine/src/cmd/file/tests.rs index 35c821d8..a2b709b2 100644 --- a/crates/engine/src/cmd/file/tests.rs +++ b/crates/engine/src/cmd/file/tests.rs @@ -121,6 +121,7 @@ fn import_audio_honours_new_tracks() { #[test] fn bounce_stems_rejects_unsupported_formats_before_writing() { let dir = std::env::temp_dir().join(format!("sc-stems-unsupported-format-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir(&dir).unwrap(); let stems = dir.join("stems"); let mut e = Engine::default(); @@ -138,6 +139,7 @@ fn bounce_stems_rejects_unsupported_formats_before_writing() { #[test] fn bounce_stems_rejects_non_string_formats_before_writing() { let dir = std::env::temp_dir().join(format!("sc-stems-format-type-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir(&dir).unwrap(); let mut e = Engine::default(); e.execute("track.new", &json!({"count": 1, "name": "Tone", "format": "mono"})).unwrap(); @@ -158,6 +160,29 @@ fn bounce_stems_rejects_non_string_formats_before_writing() { } } +#[test] +fn bounce_stems_accepts_explicit_wav_format() { + let dir = std::env::temp_dir().join(format!("sc-stems-explicit-wav-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir(&dir).unwrap(); + let mut e = Engine::default(); + let wav = tone_wav(&dir, "tone.wav"); + e.execute("file.import_audio", &json!({"path": wav})).unwrap(); + let stems = dir.join("stems"); + let result = e.execute("file.bounce_stems", &json!({"dir": stems, "format": "wav", "start": 0, "end": 32, "dither": false})).unwrap(); + let files = result["files"].as_array().unwrap(); + assert_eq!(files.len(), 1); + let path = std::path::Path::new(files[0].as_str().unwrap()); + assert_eq!(path.parent(), Some(stems.as_path())); + assert_eq!(path.extension().and_then(|extension| extension.to_str()), Some("wav")); + let (info, audio) = soundcraft_audio_io::decode(&std::fs::read(path).unwrap(), None).unwrap(); + assert_eq!(info.format, soundcraft_audio_io::FileFormat::Wav); + assert_eq!(audio.frames(), 32); + assert_eq!(audio.num_channels(), e.session().main_format().channels()); + assert!(audio.channels.iter().flatten().any(|sample| sample.abs() > 0.01)); + std::fs::remove_dir_all(&dir).unwrap(); +} + #[test] fn bounce_refuses_formats_it_cannot_write() { let mut e = Engine::default();