Merge pull request #3226 from vangel76/waveform-peaks-1722

Timeline waveform: read keyframed Audio Levels at layer time
This commit is contained in:
FredApps
2026-11-14 17:28:05 +01:00
committed by GitHub
+72 -7
View File
@@ -76,11 +76,14 @@ pub fn summary(app: &EffectcraftApp, ctx: &egui::Context, item: ItemId) -> Optio
None
}
/// Left/right gain of the layer's Audio Levels (dB) at comp time `t`; unity without the property.
fn level_gains(levels: Option<&Property>, t: Tick) -> [f32; 2] {
/// Left/right gain of `layer`'s Audio Levels (`levels`, dB) at comp time `t`; unity without the
/// property. Keyframes are in layer time, so they are read at the layer time of `t` (start
/// offset and stretch), as the mix reads them. A reversed layer (negative stretch) is still
/// drawn, though the mix leaves it silent.
fn level_gains(layer: &Layer, levels: Option<&Property>, t: Tick) -> [f32; 2] {
match levels {
Some(p) => {
let [l, r] = p.value_at(t).as_vec2();
let [l, r] = p.value_at(layer.layer_time(t)).as_vec2();
[db_to_gain(l), db_to_gain(r)]
}
None => [1.0, 1.0],
@@ -114,7 +117,7 @@ pub(crate) fn draw(p: &Painter, rect: Rect, tm: &TMap, layer: &Layer, s: &Summar
agg[2] = agg[2].min(b[2]);
agg[3] = agg[3].max(b[3]);
}
let gain = level_gains(levels, Tick::from_seconds_f64((ta + tb) * 0.5));
let gain = level_gains(layer, levels, Tick::from_seconds_f64((ta + tb) * 0.5));
for c in 0..2 {
let (lo, hi) = ((agg[c * 2] * gain[c]).clamp(-1.0, 1.0), (agg[c * 2 + 1] * gain[c]).clamp(-1.0, 1.0));
let (ya, yb) = (mid[c] - hi * half, mid[c] - lo * half);
@@ -126,14 +129,76 @@ pub(crate) fn draw(p: &Painter, rect: Rect, tm: &TMap, layer: &Layer, s: &Summar
#[cfg(test)]
mod tests {
use super::*;
use effectcraft_engine::project::Value;
use effectcraft_engine::color::Label;
use effectcraft_engine::project::{Comp, Footage, FootageKind, Keyframe, Value, build};
use effectcraft_engine::render::{FootageSource, Image, audio::mix_comp};
use effectcraft_engine::time::FrameRate;
#[test]
fn waveform_follows_audio_levels() {
let p = Property::new(1, "levels", "Audio Levels", Value::Vec2([-6.0206, -96.0]));
let g = level_gains(Some(&p), Tick::ZERO);
let mut project = Project::default();
let comp = Comp::new(64, 64, FrameRate::new(25, 1), Tick::from_seconds_f64(2.0));
let l = build::layer(&mut project, &comp, "a", LayerSource::Null, (0, 0), None);
let g = level_gains(&l, Some(&p), Tick::ZERO);
assert!((g[0] - 0.5).abs() < 1e-3, "{}", g[0]);
assert_eq!(g[1], 0.0);
assert_eq!(level_gains(None, Tick::ZERO), [1.0, 1.0]);
assert_eq!(level_gains(&l, None, Tick::ZERO), [1.0, 1.0]);
}
/// Constant 0.5 audio on both channels, at any source time.
struct Half;
impl FootageSource for Half {
fn frame(&self, _: ItemId, _: &Footage, _: Tick) -> Option<Arc<Image>> {
None
}
fn audio(&self, _: ItemId, _: &Footage, _: Tick, frames: usize, _: u32) -> Option<Vec<f32>> {
Some(vec![0.5; frames * 2])
}
}
/// #3224: keyframed Audio Levels scale the drawn waveform at the layer time the mix reads
/// them, so on a layer that starts later (or is stretched) a column's gain is the gain the
/// rendered audio has at that comp time.
#[test]
fn keyed_levels_scale_the_waveform_as_the_mix_does() {
let mut p = Project::default();
let f = Footage {
path: "x.wav".into(),
kind: FootageKind::Audio,
has_audio: true,
frame_rate: FrameRate::new(25, 1),
duration: Tick::from_seconds_f64(10.0),
..Default::default()
};
let fid = p.add_item("x.wav", Label::SeaFoam, None, ItemKind::Footage(f));
let base = Comp::new(64, 64, FrameRate::new(25, 1), Tick::from_seconds_f64(10.0));
let mut l = build::layer(&mut p, &base, "a", LayerSource::Footage { item: fid }, (0, 0), None);
// -6 dB at layer time 0 s, 0 dB at layer time 1 s.
let Some(levels) = l.props.sub_mut("audio").and_then(|g| g.get_mut("levels")) else { panic!("no Audio Levels") };
levels.keys = vec![Keyframe::new(Tick::ZERO, Value::Vec2([-6.0206, -6.0206])), Keyframe::new(Tick::from_seconds_f64(1.0), Value::Vec2([0.0, 0.0]))];
for stretch in [100.0, 200.0] {
let mut l = l.clone();
l.start_time = Tick::from_seconds_f64(2.0);
l.stretch = stretch;
l.in_point = l.start_time;
l.out_point = Tick::from_seconds_f64(9.0);
let mut comp = base.clone();
comp.layers = vec![l.clone()].into();
let cid = p.add_item("C", Label::Sandstone, None, ItemKind::Comp(comp.into()));
let levels = l.props.sub("audio").and_then(|g| g.get("levels"));
// The layer's start, a quarter and half of its ramp, its end, and after it.
for s in [2.0, 2.25, 2.5, 3.0, 4.0, 6.0] {
let t = Tick::from_seconds_f64(s);
let drawn = level_gains(&l, levels, t);
let mixed = mix_comp(&p, &Half, None, cid, t, 1, 1000);
for c in 0..2 {
let heard = mixed.get(c).copied().unwrap_or(f32::NAN) / 0.5;
assert!((drawn[c] - heard).abs() < 2e-3, "stretch {stretch}, {s} s, channel {c}: drawn {} heard {heard}", drawn[c]);
}
}
// At the layer's start (layer time 0) it is the -6 dB key, not the 0 dB one.
assert!((level_gains(&l, levels, Tick::from_seconds_f64(2.0))[0] - 0.5).abs() < 1e-3);
}
}
}