1use crate::engine::mixer_fx::BusGraph;
16use crate::media::{AudioSource, DecoderPool, SAMPLE_RATE};
17use crate::model::{Clip, ClipKind, Id, Project, Track, TrackKind, Transition};
18
19const MAX_DEPTH: usize = 8;
20
21type Ext<'a> = [Option<(f64, f64, &'a Transition, bool)>; 2];
24
25enum Dest<'a> {
27 Buf(&'a mut [f32]),
28 Buses(&'a mut BusGraph, usize),
30}
31
32impl Dest<'_> {
33 fn frames(&self) -> usize {
34 match self {
35 Dest::Buf(b) => b.len() / 2,
36 Dest::Buses(_, f) => *f,
37 }
38 }
39 fn routed(&self) -> bool {
41 matches!(self, Dest::Buses(..))
42 }
43 fn slice(&mut self, bus: Id, i0: usize, i1: usize) -> &mut [f32] {
45 match self {
46 Dest::Buf(b) => &mut b[i0 * 2..i1 * 2],
47 Dest::Buses(g, frames) => &mut g.buffer(bus, *frames)[i0 * 2..i1 * 2],
48 }
49 }
50}
51
52#[derive(Default)]
55struct Scratch(Vec<Vec<f32>>);
56
57impl Scratch {
58 fn take(&mut self, i: usize, len: usize) -> Vec<f32> {
60 if self.0.len() <= i {
61 self.0.resize_with(i + 1, Vec::new);
62 }
63 let mut b = std::mem::take(&mut self.0[i]);
64 b.clear();
65 b.resize(len, 0.0);
66 b
67 }
68 fn put(&mut self, i: usize, b: Vec<f32>) {
69 self.0[i] = b;
70 }
71}
72
73#[derive(Default)]
74pub struct Mixer {
75 scratch: Scratch,
76 graph: BusGraph,
77 order: Vec<Id>,
79}
80
81impl Mixer {
82 pub fn new() -> Self {
83 Self::default()
84 }
85
86 pub fn graph(&self) -> &BusGraph {
88 &self.graph
89 }
90
91 pub fn mix(&mut self, project: &Project, t: f64, pool: &mut DecoderPool, out: &mut [f32]) {
98 out.fill(0.0);
99 if out.len() < 2 {
100 return;
101 }
102 let Mixer { scratch, graph, order } = self;
103 if project.buses.is_empty() {
104 mix_tracks(scratch, project, &project.tracks, t, pool, &mut Dest::Buf(out), 0);
105 } else {
106 let frames = out.len() / 2;
107 graph.sync(project);
108 graph.begin(frames);
109 mix_tracks(scratch, project, &project.tracks, t, pool, &mut Dest::Buses(graph, frames), 0);
110 order.clear();
111 order.extend_from_slice(graph.order_ref());
112 for &id in order.iter() {
113 if let Some(bus) = project.bus(id) {
114 graph.flush(bus, t, out);
115 }
116 }
117 }
118 for s in out.iter_mut() {
119 *s = s.clamp(-1.0, 1.0);
120 }
121 }
122}
123
124fn mix_tracks(
126 scratch: &mut Scratch,
127 project: &Project,
128 tracks: &[Track],
129 t: f64,
130 pool: &mut DecoderPool,
131 dest: &mut Dest,
132 depth: usize,
133) {
134 let frames = dest.frames();
135 if frames == 0 {
136 return;
137 }
138 let sr = SAMPLE_RATE as f64;
139 let t_end = t + frames as f64 / sr;
140 for (ti, track) in tracks.iter().enumerate() {
141 if !active_in(tracks, ti) {
142 continue;
143 }
144 match track.kind {
145 TrackKind::Audio => {
146 for clip in &track.clips {
147 if !clip.enabled || clip.kind != ClipKind::Audio || clip.freeze.is_some() {
148 continue;
149 }
150 let ext = clip_transitions(track, clip);
151 let (estart, eend) = play_range(clip, &ext);
152 if eend <= t || estart >= t_end {
153 continue;
154 }
155 let Some(asset) = project.asset(clip.asset) else {
156 continue;
157 };
158 let i0 = (((estart.max(t) - t) * sr).round() as usize).min(frames);
159 let i1 = (((eend.min(t_end) - t) * sr).round() as usize).min(frames);
160 if i1 <= i0 {
161 continue;
162 }
163 let bus = if dest.routed() { project.bus_of(ti, clip) } else { 0 };
164 let t0 = t + i0 as f64 / sr;
165 mix_audio_clip(scratch, clip, &ext, t0, &asset.path, pool, dest.slice(bus, i0, i1), depth);
166 }
167 }
168 TrackKind::Video => {
169 for clip in &track.clips {
170 if !clip.enabled || clip.kind != ClipKind::Sequence || clip.freeze.is_some() || depth >= MAX_DEPTH {
171 continue;
172 }
173 let ext = clip_transitions(track, clip);
174 let (estart, eend) = play_range(clip, &ext);
175 if eend <= t || estart >= t_end {
176 continue;
177 }
178 let i0 = (((estart.max(t) - t) * sr).round() as usize).min(frames);
179 let i1 = (((eend.min(t_end) - t) * sr).round() as usize).min(frames);
180 if i1 <= i0 || project.sequence_tracks(clip.sequence).is_none() {
181 continue;
182 }
183 let bus = if dest.routed() { project.bus_of(ti, clip) } else { 0 };
184 mix_seq_clip(
185 scratch,
186 project,
187 clip,
188 &ext,
189 t + i0 as f64 / sr,
190 pool,
191 dest.slice(bus, i0, i1),
192 depth,
193 );
194 }
195 }
196 }
197 }
198}
199
200#[allow(clippy::too_many_arguments)]
203fn mix_audio_clip(
204 scratch: &mut Scratch,
205 clip: &Clip,
206 ext: &Ext,
207 t0: f64,
208 path: &str,
209 pool: &mut DecoderPool,
210 out: &mut [f32],
211 depth: usize,
212) {
213 let n = out.len() / 2;
214 let m = (n as f64 * clip.speed).ceil() as usize + 1;
215 let mut buf = scratch.take(depth * 2, m * 2);
216 let sr = SAMPLE_RATE as f64;
217 let s0 = if clip.reverse {
218 clip.src_time(t0) - (m - 1) as f64 / sr
220 } else {
221 clip.src_time(t0)
222 };
223 if let Some(src) = pool.audio(path, clip.audio_stream) {
224 read_block(src, s0, &mut buf);
225 resample_add(clip, ext, t0, &buf, out);
226 }
227 scratch.put(depth * 2, buf);
228}
229
230#[allow(clippy::too_many_arguments)]
235fn mix_seq_clip(
236 scratch: &mut Scratch,
237 project: &Project,
238 clip: &Clip,
239 ext: &Ext,
240 t0: f64,
241 pool: &mut DecoderPool,
242 out: &mut [f32],
243 depth: usize,
244) {
245 let n = out.len() / 2;
246 let m = (n as f64 * clip.speed).ceil() as usize + 1;
247 let mut buf = scratch.take(depth * 2 + 1, m * 2);
248 let sr = SAMPLE_RATE as f64;
249 let s0 = if clip.reverse { clip.src_time(t0) - (m - 1) as f64 / sr } else { clip.src_time(t0) };
250 if let Some(tracks) = project.sequence_tracks(clip.sequence) {
251 mix_tracks(scratch, project, tracks, s0, pool, &mut Dest::Buf(&mut buf), depth + 1);
252 resample_add(clip, ext, t0, &buf, out);
253 }
254 scratch.put(depth * 2 + 1, buf);
255}
256
257fn active_in(tracks: &[Track], i: usize) -> bool {
260 let t = &tracks[i];
261 let any_solo = tracks.iter().any(|o| o.kind == t.kind && o.solo);
262 if any_solo {
263 t.solo
264 } else {
265 !t.muted
266 }
267}
268
269fn clip_transitions<'a>(track: &'a Track, clip: &Clip) -> Ext<'a> {
272 let mut ext = [None, None];
273 for tr in &track.transitions {
274 let Some((l, r)) = track.transition_clips(tr) else { continue };
275 let Some((cut, h)) = tr.cut_half(l, r) else { continue };
276 if r.is_some_and(|r| r.id == clip.id) {
278 ext[0] = Some((cut, h, tr, true));
279 } else if l.is_some_and(|l| l.id == clip.id) {
280 ext[1] = Some((cut, h, tr, false));
281 }
282 }
283 ext
284}
285
286fn play_range(clip: &Clip, ext: &Ext) -> (f64, f64) {
288 let mut s = clip.start;
289 let mut e = clip.end();
290 if let Some((cut, h, ..)) = ext[0] {
291 s = s.min(cut - h);
292 }
293 if let Some((cut, h, ..)) = ext[1] {
294 e = e.max(cut + h);
295 }
296 (s, e)
297}
298
299fn gains(clip: &Clip, ext: &Ext, tt: f64) -> (f32, f32) {
302 let lt = clip.local(tt).clamp(0.0, clip.duration);
303 let mut g = clip.volume.at(lt) as f32 * clip.fade_mult(lt) as f32;
304 for e in ext.iter().flatten() {
305 let (cut, h, tr, is_right) = *e;
306 if tt >= cut - h && tt < cut + h {
307 let p = crate::engine::compose::trans_progress_at(tr, cut, h, tt) as f32;
308 g *= if is_right { p } else { 1.0 - p };
309 }
310 }
311 let pan = clip.pan.at(lt).clamp(-1.0, 1.0) as f32;
312 (g * (1.0 - pan).min(1.0), g * (1.0 + pan).min(1.0))
313}
314
315fn read_block(src: &mut dyn AudioSource, s0: f64, buf: &mut [f32]) {
317 if s0 >= 0.0 {
318 src.read_at(s0, buf);
319 return;
320 }
321 let skip = (((-s0) * SAMPLE_RATE as f64).round() as usize) * 2;
322 if skip >= buf.len() {
323 buf.fill(0.0);
324 return;
325 }
326 buf[..skip].fill(0.0);
327 src.read_at(0.0, &mut buf[skip..]);
328}
329
330fn resample_add(clip: &Clip, ext: &Ext, t0: f64, buf: &[f32], out: &mut [f32]) {
337 let n = out.len() / 2;
338 let m = buf.len() / 2;
339 if n == 0 || m == 0 {
340 return;
341 }
342 let (l0, r0) = gains(clip, ext, t0);
343 let (l1, r1) = gains(clip, ext, t0 + n as f64 / SAMPLE_RATE as f64);
344 let dl = (l1 - l0) / n as f32;
345 let dr = (r1 - r0) / n as f32;
346 let last = m - 1;
347 for (k, o) in out.chunks_exact_mut(2).enumerate() {
348 let pos = if clip.reverse { last as f64 - k as f64 * clip.speed } else { k as f64 * clip.speed };
349 let pos = pos.max(0.0);
350 let j = (pos as usize).min(last);
351 let j1 = (j + 1).min(last);
352 let fr = (pos - j as f64) as f32;
353 let a = &buf[j * 2..j * 2 + 2];
354 let b = &buf[j1 * 2..j1 * 2 + 2];
355 o[0] += (a[0] + (b[0] - a[0]) * fr) * (l0 + dl * k as f32);
356 o[1] += (a[1] + (b[1] - a[1]) * fr) * (r0 + dr * k as f32);
357 }
358}
359
360#[cfg(test)]
361mod tests {
362 use super::*;
363 use crate::media::{AudioSource, Backend};
364 use crate::model::{Asset, AudioStreamInfo, ClipKind, Ease, TransitionKind};
365
366 struct Const(f32);
367 impl AudioSource for Const {
368 fn duration(&self) -> f64 {
369 10.0
370 }
371 fn read_at(&mut self, t: f64, out: &mut [f32]) {
372 for (i, s) in out.iter_mut().enumerate() {
373 let tt = t + (i / 2) as f64 / SAMPLE_RATE as f64;
374 *s = if (0.0..10.0).contains(&tt) { self.0 } else { 0.0 };
375 }
376 }
377 }
378
379 struct Ramp;
381 impl AudioSource for Ramp {
382 fn duration(&self) -> f64 {
383 10.0
384 }
385 fn read_at(&mut self, t: f64, out: &mut [f32]) {
386 for (i, s) in out.iter_mut().enumerate() {
387 let tt = t + (i / 2) as f64 / SAMPLE_RATE as f64;
388 *s = if (0.0..10.0).contains(&tt) { (tt * 0.01) as f32 } else { 0.0 };
389 }
390 }
391 }
392
393 fn audio_asset(id: crate::model::Id, path: &str) -> Asset {
394 Asset {
395 id,
396 path: path.into(),
397 kind: ClipKind::Audio,
398 duration: 10.0,
399 width: 0,
400 height: 0,
401 fps: 0.0,
402 audio_streams: vec![AudioStreamInfo { index: 0, channels: 2, sample_rate: 48000, ..Default::default() }],
403 codec: "pcm".into(),
404 folder: String::new(),
405 tags: Vec::new(),
406 label: 0,
407 description: String::new(),
408 }
409 }
410
411 fn project() -> Project {
412 Project::from_media(audio_asset(0, "Z:\\nope\\fake.wav"))
413 }
414
415 fn pool() -> DecoderPool {
416 let mut p = DecoderPool::new(Backend::Ffmpeg);
417 p.insert_audio("Z:\\nope\\fake.wav", 0, Box::new(Const(0.5)));
418 p
419 }
420
421 #[test]
422 fn volume_mute_solo() {
423 let mut p = project();
424 let ai = p.audio_tracks()[0];
425 p.tracks[ai].clips[0].volume.value = 0.5;
426 let mut pool = pool();
427 let mut mx = Mixer::new();
428 let mut out = vec![1.0f32; 2048];
429 mx.mix(&p, 1.0, &mut pool, &mut out);
430 assert!(out.iter().all(|s| (s - 0.25).abs() < 1e-5), "{:?}", &out[..4]);
431
432 p.tracks[ai].muted = true;
434 mx.mix(&p, 1.0, &mut pool, &mut out);
435 assert!(out.iter().all(|s| *s == 0.0));
436 p.tracks[ai].muted = false;
437
438 let other = p.add_track(TrackKind::Audio);
440 p.tracks[other].solo = true;
441 mx.mix(&p, 1.0, &mut pool, &mut out);
442 assert!(out.iter().all(|s| *s == 0.0));
443 p.tracks[ai].solo = true;
444 mx.mix(&p, 1.0, &mut pool, &mut out);
445 assert!(out.iter().all(|s| (s - 0.25).abs() < 1e-5));
446
447 mx.mix(&p, 20.0, &mut pool, &mut out);
449 assert!(out.iter().all(|s| *s == 0.0));
450 let mut out = vec![0.0f32; 96]; mx.mix(&p, 10.0 - 0.0005, &mut pool, &mut out);
452 assert!((out[0] - 0.25).abs() < 1e-5);
453 assert_eq!(out[95], 0.0);
454 }
455
456 #[test]
457 fn bus_routing() {
458 use crate::model::{AudioFilter, FilterKind};
459 let mut p = project();
460 let ai = p.audio_tracks()[0];
461 p.main_bus();
462 let a = p.add_bus("A");
463 p.tracks[ai].bus = a;
464 let mut pool = pool();
465 let mut mx = Mixer::new();
466 let mut out = vec![0.0f32; 2 * 480];
467
468 mx.mix(&p, 1.0, &mut pool, &mut out);
470 assert!(out.iter().all(|s| (s - 0.5).abs() < 1e-5), "{}", out[0]);
471
472 p.bus_mut(a).unwrap().muted = true;
474 mx.mix(&p, 1.0, &mut pool, &mut out);
475 assert!(out.iter().all(|s| *s == 0.0), "muted bus: {}", out[0]);
476 p.bus_mut(a).unwrap().muted = false;
477
478 let b = p.add_bus("B");
480 p.bus_mut(b).unwrap().solo = true;
481 mx.mix(&p, 1.0, &mut pool, &mut out);
482 assert!(out.iter().all(|s| *s == 0.0), "other bus soloed: {}", out[0]);
483 p.bus_mut(a).unwrap().solo = true;
484 mx.mix(&p, 1.0, &mut pool, &mut out);
485 assert!(out.iter().all(|s| (s - 0.5).abs() < 1e-5), "A soloed: {}", out[0]);
486 p.bus_mut(a).unwrap().solo = false;
487 p.bus_mut(b).unwrap().solo = false;
488
489 p.bus_mut(a).unwrap().gain.value = 0.5;
491 mx.mix(&p, 1.0, &mut pool, &mut out);
492 assert!(out.iter().all(|s| (s - 0.25).abs() < 1e-5), "bus gain: {}", out[0]);
493 p.bus_mut(a).unwrap().gain.value = 1.0;
494
495 p.tracks[ai].clips[0].pan.value = -1.0;
497 mx.mix(&p, 1.0, &mut pool, &mut out);
498 assert!((out[0] - 0.5).abs() < 1e-5 && out[1].abs() < 1e-5, "{:?}", &out[..2]);
499 p.bus_mut(a).unwrap().mono = true;
500 mx.mix(&p, 1.0, &mut pool, &mut out);
501 assert!(out.iter().all(|s| (s - 0.25).abs() < 1e-5), "mono: {:?}", &out[..2]);
502 p.bus_mut(a).unwrap().mono = false;
503 p.tracks[ai].clips[0].pan.value = 0.0;
504
505 let mut f = AudioFilter::new(FilterKind::Gain);
507 f.params[0].value = -6.0;
508 p.bus_mut(a).unwrap().filters.push(f);
509 mx.mix(&p, 1.0, &mut pool, &mut out);
510 let want = 0.5 * crate::engine::mixer_fx::db_to_lin(-6.0);
511 assert!(out.iter().all(|s| (s - want).abs() < 1e-4), "bus filter: {} vs {want}", out[0]);
512 p.bus_mut(a).unwrap().filters.clear();
513
514 p.bus_mut(a).unwrap().muted = true;
516 p.tracks[ai].clips[0].bus = b;
517 mx.mix(&p, 1.0, &mut pool, &mut out);
518 assert!(out.iter().all(|s| (s - 0.5).abs() < 1e-5), "clip override: {}", out[0]);
519
520 p.remove_bus(b);
522 mx.mix(&p, 1.0, &mut pool, &mut out);
523 assert!(out.iter().all(|s| *s == 0.0), "back to the muted track bus: {}", out[0]);
524 p.tracks[ai].bus = 0;
526 mx.mix(&p, 1.0, &mut pool, &mut out);
527 assert!(out.iter().all(|s| (s - 0.5).abs() < 1e-5), "fallback to Main: {}", out[0]);
528 }
529
530 #[test]
531 fn bus_chain_sums_into_main() {
532 let mut p = project();
534 let ai = p.audio_tracks()[0];
535 p.main_bus();
536 let a = p.add_bus("A");
537 let b = p.add_bus("B");
538 p.bus_mut(a).unwrap().output = b;
539 p.bus_mut(b).unwrap().gain.value = 0.5;
540 p.tracks[ai].bus = a;
541 let mut pool = pool();
542 let mut mx = Mixer::new();
543 let mut out = vec![0.0f32; 2 * 480];
544 mx.mix(&p, 1.0, &mut pool, &mut out);
545 assert!(out.iter().all(|s| (s - 0.25).abs() < 1e-5), "{}", out[0]);
546 assert!((mx.graph().meter(a).0 - 0.5).abs() < 1e-4, "A meters pre-B: {:?}", mx.graph().meter(a));
547 assert!((mx.graph().meter(b).0 - 0.25).abs() < 1e-4, "{:?}", mx.graph().meter(b));
548 }
549
550 #[test]
551 fn ramp_and_clamp() {
552 let mut p = project();
553 let ai = p.audio_tracks()[0];
554 let c = &mut p.tracks[ai].clips[0];
555 c.volume.keys = vec![
556 crate::model::Keyframe { t: 0.0, v: 0.0, ease: Ease::Linear },
557 crate::model::Keyframe { t: 1.0, v: 2.0, ease: Ease::Linear },
558 ];
559 let mut pool = pool();
560 let mut mx = Mixer::new();
561 let mut out = vec![0.0f32; 2 * 4800]; mx.mix(&p, 0.0, &mut pool, &mut out);
563 assert!(out[0].abs() < 1e-5);
564 assert!((out[out.len() - 2] - 0.1).abs() < 1e-3, "{}", out[out.len() - 2]);
565 assert!(out[2400 * 2] > out[1200 * 2]);
566 p.tracks[ai].clips[0].volume = crate::model::Animated::new(4.0);
568 mx.mix(&p, 0.5, &mut pool, &mut out);
569 assert!(out.iter().all(|s| *s == 1.0));
570 }
571
572 #[test]
573 fn speed_reverse_freeze() {
574 let mut p = project();
575 let ai = p.audio_tracks()[0];
576 let mut pool = DecoderPool::new(Backend::Ffmpeg);
577 pool.insert_audio("Z:\\nope\\fake.wav", 0, Box::new(Ramp));
578 let mut mx = Mixer::new();
579 let mut out = vec![0.0f32; 2 * 480]; let sr = SAMPLE_RATE as f64;
581
582 p.tracks[ai].clips[0].set_speed(2.0);
584 assert!((p.tracks[ai].clips[0].duration - 5.0).abs() < 1e-9);
585 mx.mix(&p, 1.0, &mut pool, &mut out);
586 assert!((out[0] - 0.02).abs() < 1e-4, "{}", out[0]);
587 let k = 400;
588 let want = (2.0 + 2.0 * k as f64 / sr) * 0.01;
589 assert!((out[k * 2] as f64 - want).abs() < 1e-4, "{} vs {want}", out[k * 2]);
590
591 let c = &mut p.tracks[ai].clips[0];
593 c.set_speed(1.0);
594 c.reverse = true;
595 mx.mix(&p, 1.0, &mut pool, &mut out);
596 assert!((out[0] - 0.09).abs() < 1e-4, "{}", out[0]);
597 let want = (9.0 - k as f64 / sr) * 0.01;
598 assert!((out[k * 2] as f64 - want).abs() < 1e-4);
599 assert!(out[0] > out[k * 2]);
600
601 p.tracks[ai].clips[0].freeze = Some(3.0);
603 mx.mix(&p, 1.0, &mut pool, &mut out);
604 assert!(out.iter().all(|s| *s == 0.0));
605 }
606
607 #[test]
608 fn pan_and_fades() {
609 let mut p = project();
610 let ai = p.audio_tracks()[0];
611 let mut pool = pool();
612 let mut mx = Mixer::new();
613 let mut out = vec![0.0f32; 2 * 480];
614
615 p.tracks[ai].clips[0].pan.value = 0.5;
617 mx.mix(&p, 1.0, &mut pool, &mut out);
618 assert!((out[0] - 0.25).abs() < 1e-5, "{}", out[0]);
619 assert!((out[1] - 0.5).abs() < 1e-5, "{}", out[1]);
620 p.tracks[ai].clips[0].pan.value = -1.0;
622 mx.mix(&p, 1.0, &mut pool, &mut out);
623 assert!((out[0] - 0.5).abs() < 1e-5);
624 assert!(out[1].abs() < 1e-5);
625 p.tracks[ai].clips[0].pan.value = 0.0;
626
627 let c = &mut p.tracks[ai].clips[0];
629 c.fade_in = 2.0;
630 c.fade_out = 2.0;
631 mx.mix(&p, 1.0, &mut pool, &mut out);
632 assert!((out[0] - 0.25).abs() < 1e-3, "{}", out[0]);
633 mx.mix(&p, 9.5, &mut pool, &mut out);
634 assert!((out[0] - 0.125).abs() < 1e-3, "{}", out[0]);
635 mx.mix(&p, 0.0, &mut pool, &mut out);
637 assert!(out[0].abs() < 1e-3);
638 }
639
640 #[test]
643 fn fade_in_first_block_near_silent_and_rising() {
644 let mut p = project();
645 let ai = p.audio_tracks()[0];
646 p.tracks[ai].clips[0].fade_in = 0.5;
647 let mut pool = pool();
648 let mut mx = Mixer::new();
649 let mut out = vec![0.0f32; 2 * 1024]; mx.mix(&p, 0.0, &mut pool, &mut out);
651 assert!(out[0].abs() < 1e-4, "sample 0 not near-silent: {}", out[0]);
652 let mut prev = -1.0f32;
653 for s in out.chunks_exact(2).map(|s| s[0]) {
654 assert!(s + 1e-6 >= prev, "gain dipped: {s} after {prev}");
655 prev = s;
656 }
657 }
658
659 #[test]
660 fn transition_crossfade() {
661 let mut p = Project::new();
663 let a = audio_asset(0, "Z:\\nope\\a.wav");
664 let b = audio_asset(0, "Z:\\nope\\b.wav");
665 let a = p.add_asset(a);
666 let b = p.add_asset(b);
667 let ai = p.audio_tracks()[0];
668 let mut c1 = Clip::new(1000, ClipKind::Audio, "a", 0.0, 5.0);
669 c1.asset = a;
670 let mut c2 = Clip::new(1001, ClipKind::Audio, "b", 5.0, 5.0);
671 c2.asset = b;
672 c2.src_in = 5.0; let right = c2.id;
674 p.tracks[ai].clips.push(c1);
675 p.tracks[ai].clips.push(c2);
676 p.tracks[ai].transitions.push(Transition {
677 id: 2000,
678 right,
679 kind: TransitionKind::CrossFade,
680 duration: 2.0,
681 color: [0, 0, 0, 255],
682 direction: 0,
683 ease: Ease::Linear,
684 edge: Default::default(),
685 });
686 let mut pool = DecoderPool::new(Backend::Ffmpeg);
687 pool.insert_audio("Z:\\nope\\a.wav", 0, Box::new(Const(0.8)));
688 pool.insert_audio("Z:\\nope\\b.wav", 0, Box::new(Const(0.4)));
689 let mut mx = Mixer::new();
690 let mut out = vec![0.0f32; 2 * 480];
691 mx.mix(&p, 2.0, &mut pool, &mut out);
693 assert!((out[0] - 0.8).abs() < 1e-4, "{}", out[0]);
694 mx.mix(&p, 8.0, &mut pool, &mut out);
695 assert!((out[0] - 0.4).abs() < 1e-4);
696 mx.mix(&p, 4.5, &mut pool, &mut out);
698 assert!((out[0] - 0.7).abs() < 1e-3, "{}", out[0]);
699 mx.mix(&p, 5.5, &mut pool, &mut out);
701 assert!((out[0] - 0.5).abs() < 1e-3, "{}", out[0]);
702 mx.mix(&p, 5.0, &mut pool, &mut out);
704 assert!((out[0] - 0.6).abs() < 1e-3, "{}", out[0]);
705 }
706
707 fn add_transition(p: &mut Project, ti: usize, right: crate::model::Id, dur: f64) {
709 p.tracks[ti].transitions.push(Transition {
710 id: 2000,
711 right,
712 kind: TransitionKind::CrossFade,
713 duration: dur,
714 color: [0, 0, 0, 255],
715 direction: 0,
716 ease: Ease::Linear,
717 edge: Default::default(),
718 });
719 }
720
721 #[test]
722 fn transition_window_clamped_to_clips() {
723 let mut p = Project::new();
726 let a = p.add_asset(audio_asset(0, "Z:\\nope\\a.wav"));
727 let b = p.add_asset(audio_asset(0, "Z:\\nope\\b.wav"));
728 let ai = p.audio_tracks()[0];
729 let mut c1 = Clip::new(1000, ClipKind::Audio, "a", 0.0, 5.0);
730 c1.asset = a;
731 let mut c2 = Clip::new(1001, ClipKind::Audio, "b", 5.0, 1.0);
732 c2.asset = b;
733 c2.src_in = 5.0;
734 let right = c2.id;
735 p.tracks[ai].clips.push(c1);
736 p.tracks[ai].clips.push(c2);
737 add_transition(&mut p, ai, right, 8.0);
738 let mut pool = DecoderPool::new(Backend::Ffmpeg);
739 pool.insert_audio("Z:\\nope\\a.wav", 0, Box::new(Const(0.8)));
740 pool.insert_audio("Z:\\nope\\b.wav", 0, Box::new(Const(0.4)));
741 let mut mx = Mixer::new();
742 let mut out = vec![0.0f32; 2 * 480];
743 mx.mix(&p, 2.0, &mut pool, &mut out);
744 assert!((out[0] - 0.8).abs() < 1e-4, "before the clamped window: {}", out[0]);
745 mx.mix(&p, 5.0, &mut pool, &mut out);
746 assert!((out[0] - 0.6).abs() < 1e-3, "at the cut: {}", out[0]);
747 mx.mix(&p, 7.0, &mut pool, &mut out);
748 assert!(out.iter().all(|s| *s == 0.0), "audio past both clips: {}", out[0]);
749 }
750
751 #[test]
752 fn sequence_transition_crossfades_audio() {
753 let mut p = Project::new();
755 let mut seq = Vec::new();
756 for (i, path) in ["Z:\\nope\\a.wav", "Z:\\nope\\b.wav"].into_iter().enumerate() {
757 let asset = p.add_asset(audio_asset(0, path));
758 let s = p.new_sequence("s", 320, 240, 30.0);
759 let sq = p.sequence_mut(s).unwrap();
760 let sai = sq.tracks.iter().position(|t| t.kind == TrackKind::Audio).unwrap();
761 let mut inner = Clip::new(500 + i as crate::model::Id, ClipKind::Audio, "in", 0.0, 10.0);
762 inner.asset = asset;
763 sq.tracks[sai].clips.push(inner);
764 seq.push(s);
765 }
766 let vi = p.video_tracks()[0];
767 for (i, s) in seq.iter().enumerate() {
768 let mut c = Clip::new(1000 + i as crate::model::Id, ClipKind::Sequence, "sc", i as f64 * 5.0, 5.0);
769 c.sequence = *s;
770 c.src_in = i as f64 * 5.0;
771 p.tracks[vi].clips.push(c);
772 }
773 add_transition(&mut p, vi, 1001, 2.0);
774 let mut pool = DecoderPool::new(Backend::Ffmpeg);
775 pool.insert_audio("Z:\\nope\\a.wav", 0, Box::new(Const(0.8)));
776 pool.insert_audio("Z:\\nope\\b.wav", 0, Box::new(Const(0.4)));
777 let mut mx = Mixer::new();
778 let mut out = vec![0.0f32; 2 * 480];
779 mx.mix(&p, 4.5, &mut pool, &mut out);
781 assert!((out[0] - 0.7).abs() < 1e-3, "{}", out[0]);
782 mx.mix(&p, 5.5, &mut pool, &mut out);
783 assert!((out[0] - 0.5).abs() < 1e-3, "{}", out[0]);
784 mx.mix(&p, 2.0, &mut pool, &mut out);
786 assert!((out[0] - 0.8).abs() < 1e-4, "{}", out[0]);
787 mx.mix(&p, 8.0, &mut pool, &mut out);
788 assert!((out[0] - 0.4).abs() < 1e-4, "{}", out[0]);
789 }
790
791 #[test]
792 fn sequence_audio() {
793 let mut p = Project::new();
795 let aid = p.add_asset(audio_asset(0, "Z:\\nope\\fake.wav"));
796 let seq = p.new_sequence("s", 320, 240, 30.0);
797 let mut inner = Clip::new(500, ClipKind::Audio, "in", 0.0, 4.0);
798 inner.asset = aid;
799 let s = p.sequence_mut(seq).unwrap();
800 let sai = s.tracks.iter().position(|t| t.kind == TrackKind::Audio).unwrap();
801 s.tracks[sai].clips.push(inner);
802 let sc = p.insert_sequence_clip(seq, 1.0, None).expect("placed");
803 let mut pool = DecoderPool::new(Backend::Ffmpeg);
804 pool.insert_audio("Z:\\nope\\fake.wav", 0, Box::new(Ramp));
805 let mut mx = Mixer::new();
806 let mut out = vec![0.0f32; 2 * 480];
807 mx.mix(&p, 1.5, &mut pool, &mut out);
809 assert!((out[0] - 0.005).abs() < 1e-4, "{}", out[0]);
810 {
812 let c = p.clip_mut(sc).unwrap();
813 c.volume.value = 0.5;
814 c.pan.value = 1.0;
815 }
816 mx.mix(&p, 1.5, &mut pool, &mut out);
817 assert!(out[0].abs() < 1e-5, "L muted by pan, got {}", out[0]);
818 assert!((out[1] - 0.0025).abs() < 1e-4, "{}", out[1]);
819 {
820 let c = p.clip_mut(sc).unwrap();
821 c.volume.value = 1.0;
822 c.pan.value = 0.0;
823 }
824 p.clip_mut(sc).unwrap().set_speed(2.0);
826 mx.mix(&p, 1.5, &mut pool, &mut out);
827 assert!((out[0] - 0.01).abs() < 1e-4, "{}", out[0]);
828 p.clip_mut(sc).unwrap().set_speed(1.0);
829 mx.mix(&p, 0.5, &mut pool, &mut out);
831 assert!(out.iter().all(|s| *s == 0.0));
832 p.clip_mut(sc).unwrap().freeze = Some(1.0);
833 mx.mix(&p, 1.5, &mut pool, &mut out);
834 assert!(out.iter().all(|s| *s == 0.0));
835 p.clip_mut(sc).unwrap().freeze = None;
836 let vi = p.video_tracks()[0];
837 p.tracks[vi].muted = true;
838 mx.mix(&p, 1.5, &mut pool, &mut out);
839 assert!(out.iter().all(|s| *s == 0.0));
840 }
841}