diff --git a/crates/rustmotion/src/cli/commands/sheet.rs b/crates/rustmotion/src/cli/commands/sheet.rs index 2fc7184..1fd4b6a 100644 --- a/crates/rustmotion/src/cli/commands/sheet.rs +++ b/crates/rustmotion/src/cli/commands/sheet.rs @@ -224,13 +224,7 @@ pub fn cmd_sheet( )); } - if !scenario.fonts.is_empty() { - engine::renderer::load_custom_fonts(&scenario.fonts); - } - - for view in &scenario.views { - engine::prefetch_icons(&view.scenes)?; - } + engine::preload::preload_scenario_assets(&scenario)?; let start_time = std::time::Instant::now(); let config = &scenario.video; diff --git a/crates/rustmotion/src/cli/commands/still.rs b/crates/rustmotion/src/cli/commands/still.rs index 7c9da76..5698c34 100644 --- a/crates/rustmotion/src/cli/commands/still.rs +++ b/crates/rustmotion/src/cli/commands/still.rs @@ -39,13 +39,7 @@ pub fn cmd_still( format: Option, quality: u8, ) -> Result<()> { - if !scenario.fonts.is_empty() { - engine::renderer::load_custom_fonts(&scenario.fonts); - } - - for view in &scenario.views { - engine::prefetch_icons(&view.scenes)?; - } + engine::preload::preload_scenario_assets(&scenario)?; let config = &scenario.video; let fps = config.fps; diff --git a/crates/rustmotion/src/engine/preload.rs b/crates/rustmotion/src/engine/preload.rs index 9ee24fc..bf87334 100644 --- a/crates/rustmotion/src/engine/preload.rs +++ b/crates/rustmotion/src/engine/preload.rs @@ -2,7 +2,8 @@ use std::io::Read; use std::sync::Arc; use crate::components::{ChildComponent, Component}; -use crate::schema::Scene; +use crate::encode::audio_analysis::analyze_scenario_audio; +use crate::schema::{ResolvedScenario, Scene}; use rustmotion_core::engine::renderer::{ asset_cache, fetch_icon_svg, ffmpeg_available, icon_cache_dir, icon_cache_key, video_frame_cache, @@ -34,6 +35,22 @@ fn video_frame_cache_bytes() -> u64 { .sum() } +pub fn preload_scenario_assets(scenario: &ResolvedScenario) -> crate::error::Result<()> { + if !scenario.fonts.is_empty() { + rustmotion_core::engine::renderer::load_custom_fonts(&scenario.fonts); + } + for view in &scenario.views { + prefetch_icons(&view.scenes)?; + } + for failure in analyze_scenario_audio(scenario) { + eprintln!( + "rustmotion: audio-reactive: {failure} — waveform/audio_spectrum will render flat \ + for this track." + ); + } + Ok(()) +} + pub fn prefetch_icons(scenes: &[Scene]) -> crate::error::Result<()> { use std::collections::HashSet; diff --git a/crates/rustmotion/src/tests.rs b/crates/rustmotion/src/tests.rs index b404ffb..1cdf000 100644 --- a/crates/rustmotion/src/tests.rs +++ b/crates/rustmotion/src/tests.rs @@ -1596,7 +1596,7 @@ mod audio_tests { use rustmotion_core::engine::renderer::audio_analysis::{audio_analysis_cache, AudioAnalysis}; - fn make_sine_wav( + pub(super) fn make_sine_wav( total_samples: u32, sine_samples: u32, freq: f32, @@ -1629,7 +1629,7 @@ mod audio_tests { } wav } - fn nanos() -> u128 { + pub(super) fn nanos() -> u128 { std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() @@ -3770,3 +3770,55 @@ mod self_painting_background_animation_tests { ); } } + +#[cfg(test)] +mod still_runs_the_audio_analysis_tests { + use super::audio_tests::{make_sine_wav, nanos}; + use rustmotion_core::engine::renderer::audio_analysis_cache; + + fn scenario_json(wav: &str) -> String { + serde_json::json!({ + "video": { "width": 64, "height": 64, "fps": 30 }, + "audio": [{ "src": wav }], + "scenes": [{ + "duration": 1.0, + "children": [{ + "type": "audio_spectrum", "bars": 8, "mode": "bars", "color": "#ffffff", + "style": { "position": "absolute", "left": 0, "top": 0, + "width": 64, "height": 64 } + }] + }] + }) + .to_string() + } + + #[test] + fn the_shared_preload_analyses_the_track_the_way_an_encode_does() { + let sample_rate = 44100u32; + let wav = make_sine_wav(sample_rate, sample_rate / 2, 440.0, sample_rate); + let wav_path = std::env::temp_dir().join(format!("rm_still_audio_{}.wav", nanos())); + std::fs::write(&wav_path, &wav).expect("write wav fixture"); + let src = wav_path.to_str().unwrap().to_string(); + + let scenario = crate::loader::load_scenario_from_source(None, Some(&scenario_json(&src))) + .expect("load scenario"); + + audio_analysis_cache().remove(&src); + assert!( + audio_analysis_cache().get(&src).is_none(), + "test setup: the track must not already be analysed" + ); + + crate::engine::preload::preload_scenario_assets(&scenario).expect("preload"); + + let analysed = audio_analysis_cache().get(&src).is_some(); + std::fs::remove_file(&wav_path).ok(); + + assert!( + analysed, + "still and sheet went through this preamble without analysing the track, so every \ + audio-reactive component in an exported frame sat at its min — the flat bars \ + reported in issue #349" + ); + } +}