Make the audio preview follow edits that change the sound only, and start it when a silent preview gains sound (#2104)

This commit is contained in:
MarekSpodBiedry committed 2026-11-04 07:55:33 +01:00
1 parent 7b5786fdb6
commit 4966534c82
4 files changed
+57 -2

No files matched your search

+1
View File
@@ -40,6 +40,7 @@ impl EffectcraftApp {
restore_max,
audio_frame: (!plan.video).then_some(plan.first),
audio_held: false,
silent_project: None,
starting: false,
starting_since: now,
first_cached: None,
+16
View File
@@ -70,6 +70,7 @@ impl EffectcraftApp {
let step = plan.map_or(1, |p| p.step);
let fit_ahead = self.frames_that_fit(&c, scale) - 1;
self.prefetch_playback(&pass, fit_ahead);
self.wake_silent_preview();
self.pump_meters(ctx, now);
if !self.playback.playing {
self.cache_when_idle(ctx, &c, &series, now, input_active, (wa, wb));
@@ -254,6 +255,21 @@ impl EffectcraftApp {
false
}
/// A silent preview whose project was edited so that it has sound now (a layer unmuted while
/// playing, #2104): hold the sound for [`Self::start_held_audio`], which starts it from the
/// current frame once the frames ahead are cached. Checked once per project edit.
fn wake_silent_preview(&mut self) {
let pb = &self.playback;
if self.audio.is_some() || !pb.playing || pb.caching || pb.starting || pb.audio_held || !pb.plan.is_some_and(|p| p.video) {
return;
}
if pb.silent_project.as_ref().is_some_and(|p| std::sync::Arc::ptr_eq(p, &self.session.project)) {
return;
}
self.playback.silent_project = Some(self.session.project.clone());
self.playback.audio_held = self.audio_plays();
}
/// Held sound starts once the frames ahead are cached again after a stall (see
/// `Playback::audio_held`): the prefetch window when the silent preview keeps real time, else
/// the rest of the preview.
+38 -2
View File
@@ -23,7 +23,7 @@ use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use std::thread::JoinHandle;
use effectcraft_engine::project::{ItemId, ItemKind, LayerSource, Project};
use effectcraft_engine::project::{GroupKind, ItemId, ItemKind, Layer, LayerSource, Project, PropGroup};
use effectcraft_engine::time::{FrameRate, Tick};
use crate::frames::RenderSource;
@@ -342,7 +342,7 @@ fn audio_changed(old: &Project, new: &Project, comp: ItemId) -> bool {
|| x.switches.solo != y.switches.solo
|| x.switches.effects != y.switches.effects
|| x.props.sub("audio") != y.props.sub("audio")
|| x.effects() != y.effects()
|| audio_effects(x) != audio_effects(y)
|| match &x.source {
LayerSource::Footage { item } => footage_differs(old, new, *item),
LayerSource::Comp { item } => comps_differ(old, new, *item, depth + 1),
@@ -350,6 +350,12 @@ fn audio_changed(old: &Project, new: &Project, comp: ItemId) -> bool {
}
})
}
/// The layer's audio effects (Effect > Audio), the only effects the mixer applies; a video effect
/// edit leaves them as they were.
fn audio_effects(l: &Layer) -> Vec<&PropGroup> {
let is_audio = |g: &&PropGroup| matches!(&g.kind, GroupKind::Effect { effect } if effectcraft_engine::effects::audio_fx::is_audio_effect(effect));
l.effects().into_iter().flat_map(|fx| fx.groups()).filter(is_audio).collect()
}
/// The footage item's audio availability changed (relinked, missing, no audio).
fn footage_differs(old: &Project, new: &Project, item: ItemId) -> bool {
let f = |p: &Project| match p.item(item).map(|i| &i.kind) {
@@ -763,6 +769,36 @@ mod tests {
}
}
/// #2104: a video effect edit leaves the mix alone, an audio effect changes it.
#[test]
fn only_audio_effects_change_the_audio() {
use effectcraft_engine::color::Label;
use effectcraft_engine::project::build::{self, Ids};
use effectcraft_engine::project::{Comp, Footage, FootageKind};
let mut p = Project::default();
let footage =
Footage { path: "tone.flac".into(), kind: FootageKind::Audio, duration: Tick::from_seconds_f64(60.0), has_audio: true, ..Default::default() };
let item = p.add_item("tone.flac", Label::SeaFoam, None, ItemKind::Footage(footage));
let mut comp = Comp::new(64, 36, FrameRate::new(30, 1), Tick::from_seconds_f64(10.0));
let layer = build::layer(&mut p, &comp, "tone.flac", LayerSource::Footage { item }, (0, 0), None);
comp.layers = vec![layer].into();
let cid = p.add_item("Comp", Label::Sandstone, None, ItemKind::Comp(comp.into()));
let with_effect = |id: &str| {
let mut edited = p.clone();
let spec = effectcraft_engine::effects::find(id).expect("the effect exists");
let mut next = edited.next_id;
let g = effectcraft_engine::effects::instantiate(spec, &mut Ids(&mut next), spec.name, [64.0, 36.0]);
edited.next_id = next;
if let Some(fx) = edited.comp_mut(cid).and_then(|c| c.layers.first_mut()).and_then(|l| l.props.sub_mut("effects")) {
fx.children.push(g.into());
}
edited
};
assert!(!audio_changed(&p, &with_effect("ec.blur.gaussian"), cid), "a video effect must not drop the sound");
assert!(audio_changed(&p, &with_effect("ec.audio.backwards"), cid), "an audio effect must be followed");
}
#[test]
fn meter_decays_and_holds_peaks() {
let mut m = Meter::default();
+2
View File
@@ -196,6 +196,8 @@ pub struct Playback {
/// 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,
/// The project last checked for a silent preview that became audible (#2104).
pub silent_project: Option<Arc<effectcraft_engine::project::Project>>,
/// Starting a preview with sound (#947): the playhead stays on the first frame while the
/// frames ahead render (the prefetch window, at most what fits in the RAM preview), then
/// sound and picture start together from it. Without this the picture played on silently