Merge pull request #658 from vbasky/fix/current-bugs

Fix silent WAV sample-rate substitution
This commit is contained in:
Brandon Thomas 2026-10-10 14:57:13 -04:00 committed by GitHub
commit 794a25fc9e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 60 additions and 10 deletions

View File

@ -64,6 +64,21 @@ fn export_integer_parameters_cannot_wrap_or_overflow() {
assert!(s.execute("export.resolve", json!({"bitrateKbps":8000.0, "keyframeDistance":48.0})).is_ok(), "integer-valued floats are integers");
}
#[test]
fn wav_resolve_honours_explicit_sample_rates() {
let mut s = demo();
for rate in [4000, 192_000, 192_001, 200_000, 352_800, 384_000] {
let result = s.execute("export.resolve", json!({"format":"wav", "settings":{"audio":{"sample_rate":rate}}})).unwrap();
assert_eq!(result["settings"]["audio"]["sample_rate"], rate);
assert_eq!(result["output"]["sampleRate"], rate);
}
let result = s.execute("export.resolve", json!({"format":"wav", "settings":{"audio":{"sample_rate":null}}})).unwrap();
assert_eq!(result["output"]["sampleRate"], 48_000);
for rate in [0, 384_001] {
assert!(s.execute("export.resolve", json!({"format":"wav", "settings":{"audio":{"sample_rate":rate}}})).is_err());
}
}
#[test]
fn nested_camel_case_settings_are_honoured() {
let mut s = demo();

View File

@ -410,16 +410,7 @@ impl ExportSettings {
6.. => 6,
_ => 2,
};
Resolved {
width: w,
height: h,
rate,
sample_rate: self.audio.sample_rate.filter(|r| (8000..=192_000).contains(r)).unwrap_or(seq_sr),
channels,
target_kbps: target,
max_kbps: max,
keyint,
}
Resolved { width: w, height: h, rate, sample_rate: self.audio.sample_rate.unwrap_or(seq_sr), channels, target_kbps: target, max_kbps: max, keyint }
}
/// The audio codec actually used.

View File

@ -167,6 +167,50 @@ fn wav_and_aiff_audio_only() {
}
}
#[test]
fn wav_export_honours_explicit_sample_rates() {
let (project, sequence, sources) = matte([0.0, 0.0, 0.0, 1.0], 64, 36, Some(-6.0));
let dir = Scratch::new("wav-sample-rates");
for rate in [1, 4000, 7350, 8000, 48_000, 192_000, 192_001, 200_000, 352_800, 384_000] {
let settings = ExportSettings {
format: Format::Wav,
path: dir.path(&format!("{rate}.wav")),
audio: AudioSettings { sample_rate: Some(rate), ..Default::default() },
range: Some(TimeRange::new(Tick::ZERO, Tick(TICKS_PER_SECOND / 4))),
..Default::default()
};
settings.validate().unwrap();
let resolved = settings.resolve(64, 36, FrameRate::FPS_24, 48_000);
assert_eq!(resolved.sample_rate, rate, "requested {rate} Hz");
export(&project, sequence, &settings, &sources, &Progress::default()).unwrap();
let bytes = std::fs::read(&settings.path).unwrap();
assert_eq!(u32::from_le_bytes(bytes[24..28].try_into().unwrap()), rate);
assert_eq!(u32::from_le_bytes(bytes[40..44].try_into().unwrap()), rate / 4 * 4, "quarter-second stereo 16-bit PCM");
if let Some(probe) = ffprobe_json(&["-show_streams"], &settings.path) {
assert_eq!(probe["streams"][0]["sample_rate"], rate.to_string());
}
}
let settings = ExportSettings { format: Format::Wav, ..Default::default() };
assert_eq!(settings.resolve(64, 36, FrameRate::FPS_24, 44_100).sample_rate, 44_100);
}
#[test]
fn aac_export_rejects_unsupported_explicit_sample_rates() {
let (project, sequence, sources) = matte([0.0, 0.0, 0.0, 1.0], 64, 36, Some(-6.0));
let dir = Scratch::new("aac-sample-rates");
for rate in [4000, 192_001, 200_000, 384_000] {
let settings = ExportSettings {
path: dir.path(&format!("{rate}.mp4")),
audio: AudioSettings { sample_rate: Some(rate), ..Default::default() },
range: Some(TimeRange::new(Tick::ZERO, Tick(TICKS_PER_SECOND / 4))),
..Default::default()
};
let error = export(&project, sequence, &settings, &sources, &Progress::default()).unwrap_err();
assert!(error.to_string().contains("sample rate"), "{error}");
assert!(!Path::new(&settings.path).exists(), "an unsupported rate must not produce a substitute file");
}
}
#[test]
fn invalid_export_allocations_are_rejected_before_rendering() {
let (project, sequence, _) = matte([1.0, 0.0, 0.0, 1.0], 64, 36, None);