mirror of
https://github.com/storytold/filmcraft.git
synced 2026-10-14 19:26:59 -04:00
fix(audio): crossfade audio transitions through the clip handles (fixes #1473)
This commit is contained in:
1 parent
71438c627b
commit
e22a49db1c
2 files changed
+70
-5
No files matched your search
@@ -109,9 +109,11 @@ pub(crate) fn track_input_at(
|
||||
if !tr.range().overlaps(&range) {
|
||||
continue;
|
||||
}
|
||||
// Re-mix: recompute the covered samples with crossfade gains.
|
||||
let from = tr.from.and_then(|id| track.item(id));
|
||||
let to = tr.to.and_then(|id| track.item(id));
|
||||
// Re-mix: recompute the covered samples with crossfade gains. Both sides play over the
|
||||
// whole transition, into their handles as the picture does, so a transition centred on a
|
||||
// cut overlaps the two clips instead of only fading each on its own side of the cut.
|
||||
let from = tr.from.and_then(|id| track.item(id)).map(|i| with_handles(i, tr.start, tr.end()));
|
||||
let to = tr.to.and_then(|id| track.item(id)).map(|i| with_handles(i, tr.start, tr.end()));
|
||||
let s0 = tr.start.to_units_floor(sr as i64).max(start);
|
||||
let s1 = tr.end().to_units_floor(sr as i64).min(start + frames as i64);
|
||||
if s1 <= s0 {
|
||||
@@ -124,10 +126,10 @@ pub(crate) fn track_input_at(
|
||||
}
|
||||
let mut a = AudioBuffer::silence(sr, w, n);
|
||||
let mut b = AudioBuffer::silence(sr, w, n);
|
||||
if let Some(f) = from {
|
||||
if let Some(f) = &from {
|
||||
mix_item(project, f, s0, n, sr, sources, &mut a, 1.0, &live);
|
||||
}
|
||||
if let Some(tt) = to {
|
||||
if let Some(tt) = &to {
|
||||
mix_item(project, tt, s0, n, sr, sources, &mut b, 1.0, &live);
|
||||
}
|
||||
let dur = (tr.end() - tr.start).to_units_floor(sr as i64).max(1) as f32;
|
||||
@@ -143,6 +145,25 @@ pub(crate) fn track_input_at(
|
||||
tbuf
|
||||
}
|
||||
|
||||
/// `item` lengthened to cover `[t0, t1)` with its media timing unchanged: the part before its in
|
||||
/// point and after its out point plays the source's handles (silent where the source has none).
|
||||
/// Remixed clips have no linear media to extend and stay as they are.
|
||||
fn with_handles(item: &TrackItem, t0: Tick, t1: Tick) -> TrackItem {
|
||||
let mut ext = item.clone();
|
||||
if crate::remix::is_remixed(item) {
|
||||
return ext;
|
||||
}
|
||||
let head = (item.start - t0).max(Tick::ZERO);
|
||||
let tail = (t1 - item.end()).max(Tick::ZERO);
|
||||
ext.start = item.start - head;
|
||||
ext.duration = item.duration + head + tail;
|
||||
// forward media time is source_in + (t - start)·speed; reversed it counts down from the out
|
||||
// point, so the extra head (forward) or tail (reverse) moves source_in back
|
||||
let shift = if item.reverse { tail } else { head };
|
||||
ext.source_in = item.source_in - Tick((shift.0 as f64 * item.speed.abs()).round() as i64);
|
||||
ext
|
||||
}
|
||||
|
||||
/// Held Clip Mixer controls of one track.
|
||||
#[derive(Default)]
|
||||
struct ClipLive {
|
||||
|
||||
@@ -7,6 +7,7 @@ use filmcraft_media::{AudioStreamInfo, FrameRequest, MediaError, MediaInfo, Medi
|
||||
use filmcraft_project::mixer::{LANE_MUTE, LANE_PAN, LANE_VOLUME, send_lane};
|
||||
use filmcraft_project::{
|
||||
AudioChannels, AutomationMode, ItemId, ItemKind, Keyframe, Label, MediaClip, MediaRef, ParamValue, Project, SequenceSettings, TrackKind, TrackSend,
|
||||
Transition, TransitionId,
|
||||
};
|
||||
use filmcraft_time::{FrameRate, TICKS_PER_SECOND, Tick, TimeRange};
|
||||
|
||||
@@ -406,6 +407,49 @@ fn audio_transition_curves() {
|
||||
}
|
||||
}
|
||||
|
||||
/// A transition centred on a cut mixes both clips across its whole length: the outgoing clip plays
|
||||
/// on past its out point and the incoming one starts before its in point (their handles), as the
|
||||
/// picture does, so the two overlap instead of each just dipping at the cut. The incoming source is
|
||||
/// a ramp, so its handle must come from the media just before its in point.
|
||||
#[test]
|
||||
fn audio_transition_on_a_cut_crossfades_through_the_handles() {
|
||||
fn ramp(i: i64, _: usize) -> f32 {
|
||||
i as f32 * 1e-6
|
||||
}
|
||||
let mut r = Rig::new(&[dc1, ramp]);
|
||||
let (a_src, b_src) = (r.track(0).items[0].item, r.track(1).items[0].item);
|
||||
let sec = |s: f64| Tick::from_seconds_f64(s);
|
||||
let a = r.p.make_track_item(a_src, TrackKind::Audio, Tick::ZERO, TimeRange::new(Tick::ZERO, sec(1.0)), FrameRate::FPS_24).unwrap();
|
||||
let b = r.p.make_track_item(b_src, TrackKind::Audio, sec(1.0), TimeRange::new(sec(2.0), sec(1.0)), FrameRate::FPS_24).unwrap();
|
||||
let effect = filmcraft_project::vtransition::find_transition("constant_gain", filmcraft_project::EffectKind::AudioTransition).unwrap().instance();
|
||||
let tr = Transition {
|
||||
id: TransitionId(1),
|
||||
effect,
|
||||
start: sec(0.75),
|
||||
duration: sec(0.5),
|
||||
from: Some(a.id),
|
||||
to: Some(b.id),
|
||||
align: Default::default(),
|
||||
reverse: false,
|
||||
};
|
||||
r.track(1).items.clear();
|
||||
let t0 = r.track(0);
|
||||
t0.items = vec![a, b];
|
||||
t0.transitions.push(tr);
|
||||
// the incoming clip plays media sample 96 000 at the cut (sample 48 000)
|
||||
let b_at = |s: i64| ramp(s + 48_000, 0);
|
||||
// constant gain at p = 0.25, 0.5 (the cut), 0.75: (1 - p)·A + p·B
|
||||
for (s, p) in [(42_000, 0.25), (48_000, 0.5), (54_000, 0.75)] {
|
||||
let want = (1.0 - p) * 0.1 + p * b_at(s);
|
||||
let [l, rr] = r.mix(s, 1);
|
||||
assert!(close(l[0], want, 1e-5) && close(rr[0], want, 1e-5), "sample {s}: {} (want {want})", l[0]);
|
||||
}
|
||||
// outside the transition each clip plays alone, at full level
|
||||
for (s, want) in [(35_000, 0.1), (61_000, b_at(61_000))] {
|
||||
assert!(close(r.mix(s, 1)[0][0], want, 1e-6), "sample {s}");
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------- 5.1
|
||||
|
||||
/// Distinct DC level per source channel: 0.1, 0.2, … (L, R, C, LFE, Ls, Rs for 6-channel sources).
|
||||
|
||||
Reference in new issue
Block a user