Preview: sound starts once rendering keeps real time (#208)
Since #103, a preview with audio stayed silent until every frame from the playhead to the end of the preview (or what fits in the RAM preview) was cached. The prefetch only renders a few frames ahead, so on a normal work area the sound came near the end or on the second loop, even when the comp rendered in real time. The held sound now also starts once the silent preview has kept real time for a quarter second with the prefetch window ahead cached. A comp that can't render in real time still waits for the cache, as #103 intended; a stall clears the measured rate so the sound doesn't cut in and out. Test: preview_with_audio_sounds_once_rendering_keeps_up (a 30 s comp sounds within 2 s, without skipping a frame first). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018gnTnH3Jmk5zr4tMq1Mc9f
This commit is contained in:
parent
e87e1b763e
commit
6e31677718
|
|
@ -139,7 +139,8 @@ pub struct Playback {
|
|||
pub audio_frame: Option<i64>,
|
||||
/// The preview's sound waits for frames: while the frames ahead aren't cached, every frame
|
||||
/// shows as it renders, silently; the sound starts once the rest of the preview (or what
|
||||
/// fits in the RAM preview) is cached, so it never plays over skipped frames (#103).
|
||||
/// fits in the RAM preview) is cached, so it never plays over skipped frames (#103), or
|
||||
/// sooner once the silent preview keeps real time with the frames ahead cached (#208).
|
||||
pub audio_held: bool,
|
||||
/// When the last frames were shown (seconds, the last second's worth): the achieved rate.
|
||||
pub shown_at: std::collections::VecDeque<f64>,
|
||||
|
|
@ -168,6 +169,15 @@ impl Playback {
|
|||
let step = self.plan.map_or(1, |p| p.step).max(1) as f64;
|
||||
self.achieved_fps().map(|f| f * step >= self.fps * 0.95)
|
||||
}
|
||||
|
||||
/// The preview has kept real time for the last quarter second (at least two frames) up to
|
||||
/// `now`: rendering keeps up, so sound can start without waiting for the whole preview.
|
||||
pub fn keeps_up(&self, now: f64) -> bool {
|
||||
let step = self.plan.map_or(1, |p| p.step).max(1) as f64;
|
||||
let frame = step / self.fps.max(1.0);
|
||||
let (Some(a), Some(b)) = (self.shown_at.front(), self.shown_at.back()) else { return false };
|
||||
self.shown_at.len() >= 3 && b - a >= (2.0 * frame).max(0.25) && now - b <= 2.0 * frame && self.real_time() == Some(true)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EffectcraftApp {
|
||||
|
|
@ -1016,10 +1026,12 @@ impl EffectcraftApp {
|
|||
return;
|
||||
}
|
||||
let snap = |f: i64| wa + (f - wa).div_euclid(step) * step;
|
||||
// Held sound starts once the frames ahead are cached (see `Playback::audio_held`).
|
||||
// Held sound starts once the frames ahead are cached (see `Playback::audio_held`): the
|
||||
// prefetch window when the silent preview keeps real time, else the rest of the preview.
|
||||
let fit = self.frames_that_fit(&c, scale);
|
||||
if self.playback.audio_held
|
||||
&& let Some(p) = plan
|
||||
&& self.cached_ahead(&p, &series, cur, self.frames_that_fit(&c, scale))
|
||||
&& ((self.playback.keeps_up(now) && self.cached_ahead(&p, &series, cur, ahead.min(fit))) || self.cached_ahead(&p, &series, cur, fit))
|
||||
{
|
||||
self.playback.audio_held = false;
|
||||
self.playback.start_wall = now;
|
||||
|
|
@ -1052,6 +1064,8 @@ impl EffectcraftApp {
|
|||
self.audio = None;
|
||||
self.playback.audio_held = true;
|
||||
self.playback.waiting = true;
|
||||
// The rate measured before the stall no longer says it keeps up.
|
||||
self.playback.shown_at.clear();
|
||||
self.playback.start_wall = now;
|
||||
self.playback.start_frame = cur;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,10 +30,15 @@ impl AudioDevice for Fake {
|
|||
}
|
||||
}
|
||||
|
||||
/// A 30 fps comp whose solid sounds a tone (Audio switch on).
|
||||
/// A 2 s, 30 fps comp whose solid sounds a tone (Audio switch on).
|
||||
fn setup(audible: bool, fake: &Fake) -> (EffectcraftApp, ItemId) {
|
||||
setup_long(audible, fake, 2.0)
|
||||
}
|
||||
|
||||
/// A `duration`-second, 30 fps comp whose solid sounds a tone (Audio switch on).
|
||||
fn setup_long(audible: bool, fake: &Fake, duration: f64) -> (EffectcraftApp, ItemId) {
|
||||
let mut s = Session::default();
|
||||
s.execute("comp.new", json!({"name": "A", "width": 64, "height": 36, "frameRate": 30, "duration": 2})).unwrap();
|
||||
s.execute("comp.new", json!({"name": "A", "width": 64, "height": 36, "frameRate": 30, "duration": duration})).unwrap();
|
||||
let cid = s.active_comp_id().unwrap();
|
||||
let l = s.execute("layer.newSolid", json!({"color": "#808080"})).unwrap()["layer"].as_u64().unwrap();
|
||||
if audible {
|
||||
|
|
@ -140,8 +145,41 @@ fn preview_with_audio_shows_every_frame_and_sounds_once_cached() {
|
|||
}
|
||||
assert!(h.state().audio.is_some() && fake.0.lock().unwrap().0.is_some(), "the sound started once the frames ahead were cached");
|
||||
assert!(shown > 0, "frames played silently first");
|
||||
// A frame that isn't cached stops the sound rather than being skipped.
|
||||
// A frame that isn't cached stops the sound rather than being skipped. (Frames render fast
|
||||
// enough here to be cached again before the check: keep them out of the cache.)
|
||||
h.state().frames.set_budget(0);
|
||||
h.state().frames.clear();
|
||||
h.step();
|
||||
assert!(h.state().audio.is_none() && h.state().playback.audio_held && h.state().playback.playing);
|
||||
}
|
||||
|
||||
/// A preview whose frames render in real time sounds without waiting for the whole work area to
|
||||
/// be cached (#208): once the silent preview has kept up for a moment with the frames ahead
|
||||
/// cached, the sound starts, still without skipping a frame before it.
|
||||
#[test]
|
||||
fn preview_with_audio_sounds_once_rendering_keeps_up() {
|
||||
let fake = Fake::default();
|
||||
let (app, cid) = setup_long(true, &fake, 30.0);
|
||||
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).with_step_dt(1.0 / 30.0).build_eframe(|_| app);
|
||||
h.run_steps(2);
|
||||
let frame = |h: &Harness<'_, EffectcraftApp>| {
|
||||
let app = h.state();
|
||||
app.session.project.comp(cid).unwrap().frame_rate.frame_at(app.session.time())
|
||||
};
|
||||
let now = h.ctx.input(|i| i.time);
|
||||
h.state_mut().play(now);
|
||||
assert!(h.state().playback.audio_held && h.state().audio.is_none(), "nothing is cached: no sound yet");
|
||||
let mut last = frame(&h);
|
||||
// Two seconds of playback, each step giving the frame workers time to keep up.
|
||||
for _ in 0..60 {
|
||||
h.step();
|
||||
if h.state().audio.is_some() {
|
||||
break;
|
||||
}
|
||||
let f = frame(&h);
|
||||
assert!(f == last || f == last + 1, "{last} → {f}: a frame was skipped");
|
||||
last = f;
|
||||
std::thread::sleep(std::time::Duration::from_millis(20));
|
||||
}
|
||||
assert!(h.state().audio.is_some() && fake.0.lock().unwrap().0.is_some(), "the sound started within 2 s, at frame {last} of 900");
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue