1use std::collections::HashMap;
17
18use alpha2_audio::{
19 Biquad, BiquadKind, ChannelLayout, Compressor, Ducker, Interpolator, LoudnessMeter,
20 LoudnessReport, PeakLimiter, ProcessCtx, Processor, ProcessorChain, SchroederReverb, Stretch,
21 TransportState,
22};
23use alpha2_eval::{ActiveAudioClip, AudioSpanScratch, sample_scalar};
24use alpha2_media::{AudioBlock, AudioBlockProvider, SourceId, SpeakerLayout};
25use alpha2_model::{
26 Animated, AudioEffect, ClipId, EqBandKind, LOOP_FADE_SECONDS, LoopMode, MAX_GAIN, MasterAudio,
27 TimeMap, TrackAudio, TrackId,
28};
29use alpha2_time::{MediaTime, TICKS_PER_SECOND, TimeRange};
30
31use crate::downmix::{ChannelMix, Downmix};
32use crate::{EngineError, EngineProject};
33
34mod stretch_read;
35
36use stretch_read::{StretchSlot, stretched_request, stretches};
37
38const MAX_CACHED_WEIGHTS: usize = 1 << 15;
40
41pub const MAX_AUDIO_BLOCK: usize = 8_192;
43
44const _: () = {
47 let rate = *alpha2_model::SAMPLE_RATE_RANGE.start() as usize;
48 let samples = MAX_AUDIO_BLOCK + 2 + alpha2_model::MAX_CHAIN_EFFECTS;
49 assert!(
50 samples * 1_000 <= alpha2_model::AUDIO_EFFECT_STATE_MARGIN_MS as usize * rate,
51 "AUDIO_EFFECT_STATE_MARGIN_MS must hold a largest block at the lowest rate"
52 );
53};
54
55pub const MAX_SETTLING_SECONDS: u32 = 30;
61
62#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
65pub enum AudioContinuity {
66 #[default]
70 Exact,
71 ContinueSmallGaps,
77}
78
79#[derive(Clone, Debug, PartialEq, Eq)]
81pub struct AudioRenderStatus {
82 pub start_sample: i64,
84 pub frames: usize,
86 pub missing_sources: Vec<SourceId>,
93}
94
95impl AudioRenderStatus {
96 #[must_use]
98 pub fn is_complete(&self) -> bool {
99 self.missing_sources.is_empty()
100 }
101}
102
103#[derive(Clone, Debug, PartialEq, Eq)]
105pub struct AudioNeed {
106 pub source_id: SourceId,
108 pub stream: Option<u32>,
111 pub source_range: TimeRange,
113}
114
115struct TrackBus {
116 id: TrackId,
117 audio: TrackAudio,
118 chain: ProcessorChain,
119 ducker: Option<BusDucker>,
120 sidechain: Option<usize>,
121 output: [Vec<f32>; 2],
123 live: bool,
126 latency: i64,
128 clips: Vec<ClipSlot>,
130 chain_lead: i64,
132 ducker_lead: i64,
134 read: bool,
136}
137
138impl TrackBus {
139 fn active_sidechain(&self, index: usize) -> Option<usize> {
141 self.ducker.as_ref()?;
142 self.sidechain.filter(|&sidechain| sidechain != index)
143 }
144
145 fn make_live(&mut self, channels: usize, frames: usize) {
147 if self.live {
148 return;
149 }
150 for plane in self.output.iter_mut().take(channels) {
151 plane[..frames].fill(0.0);
152 }
153 self.live = true;
154 #[cfg(test)]
155 BUS_PLANES_CLEARED.set(BUS_PLANES_CLEARED.get() + channels);
156 }
157}
158
159struct BusDucker {
161 ducker: Ducker,
162 output: [Vec<f32>; 2],
163}
164
165struct ClipSlot {
166 id: ClipId,
167 chain: Option<ClipChain>,
168}
169
170struct ClipChain {
176 effects: Vec<AudioEffect>,
177 chain: Option<ProcessorChain>,
178 ran_in: u64,
180 latency: i64,
182 history: u32,
184 next_input: Option<i64>,
186 lead: i64,
188}
189
190pub struct AudioRenderer {
192 layout: ChannelLayout,
193 sample_rate: u32,
194 max_block: usize,
195 buses: Vec<TrackBus>,
197 bus_of_track: HashMap<TrackId, usize>,
199 slot_of_clip: HashMap<ClipId, (usize, usize)>,
201 span_scratch: AudioSpanScratch,
202 master_chain: ProcessorChain,
203 master_latency: i64,
205 lookahead: i64,
207 history: i64,
209 master_lead: i64,
211 limiter_lead: i64,
213 stage_leads: Vec<i64>,
216 master: MasterAudio,
217 limiter: Option<PeakLimiter>,
218 normalization_gain: f32,
220 hold_incomplete: bool,
221 continuity: AudioContinuity,
222 meter: Option<LoudnessMeter>,
224 output_meter: Option<LoudnessMeter>,
226 any_solo: bool,
227 clip_a: [Vec<f32>; 2],
228 clip_b: [Vec<f32>; 2],
229 bus_in: [Vec<f32>; 2],
230 mix: [Vec<f32>; 2],
231 output: [Vec<f32>; 2],
232 envelope: [Vec<f32>; 3],
234 next_sample: Option<i64>,
236 live_chains: Vec<(usize, usize)>,
238 blocks_mixed: u64,
239 weights: Vec<f64>,
241 phase_weights: Vec<f64>,
243 stretch: Stretch,
245 stretch_slots: HashMap<ClipId, StretchSlot>,
246 #[cfg(test)]
248 most_held: (usize, u64),
249 #[cfg(test)]
251 samples_read: usize,
252}
253
254#[derive(Clone, Copy, Debug, PartialEq, Eq)]
256enum Fill {
257 Samples,
259 Presence,
261}
262
263impl AudioRenderer {
264 pub fn new(
271 snapshot: &EngineProject,
272 channels: usize,
273 max_block: usize,
274 ) -> Result<Self, EngineError> {
275 let layout =
276 ChannelLayout::from_channels(channels).ok_or(EngineError::InvalidAudioConfiguration)?;
277 let project = snapshot.project();
278 let sample_rate = project.settings.sample_rate;
279 if sample_rate == 0 || max_block == 0 || max_block > MAX_AUDIO_BLOCK {
280 return Err(EngineError::InvalidAudioConfiguration);
281 }
282
283 let (mut buses, bus_of_track) = track_buses(project, layout, sample_rate, max_block);
284 resolve_sidechains(&mut buses, &bus_of_track);
285 let slot_of_clip = buses
286 .iter()
287 .enumerate()
288 .flat_map(|(bus, track)| {
289 track
290 .clips
291 .iter()
292 .enumerate()
293 .map(move |(position, slot)| (slot.id.clone(), (bus, position)))
294 })
295 .collect();
296
297 let mut master_chain = ProcessorChain::new(layout);
298 push_effects(&mut master_chain, &project.audio.effects);
299 master_chain.prepare(sample_rate, max_block);
300 let limiter = project.audio.limiter.map(|limiter| {
301 let mut peak = PeakLimiter::new(limiter.threshold, limiter.release_ms);
302 peak.prepare(sample_rate, max_block);
303 peak
304 });
305 let any_solo = project.timeline.any_solo();
306 read_buses(&mut buses, any_solo);
307 let master_latency = i64::from(master_chain.latency_samples());
308 let lookahead = master_latency + longest_path_latency(&buses);
309 let settling = Settling(i64::from(sample_rate) * i64::from(MAX_SETTLING_SECONDS));
310 let limiter_lead = settling.lead(0, limiter.as_ref().map_or(0, Processor::history_samples));
311 let master_lead = settling.lead(limiter_lead, master_chain.history_samples());
312 let history = assign_leads(&mut buses, master_lead, settling).max(master_lead);
313 let stage_leads = stage_leads(&buses, master_lead, limiter_lead);
314
315 Ok(Self {
316 layout,
317 sample_rate,
318 max_block,
319 buses,
320 bus_of_track,
321 slot_of_clip,
322 span_scratch: AudioSpanScratch::default(),
323 master_chain,
324 master_latency,
325 lookahead,
326 history,
327 master_lead,
328 limiter_lead,
329 stage_leads,
330 master: project.audio.clone(),
331 limiter,
332 normalization_gain: 1.0,
333 hold_incomplete: false,
334 continuity: AudioContinuity::Exact,
335 meter: None,
336 output_meter: None,
337 any_solo,
338 clip_a: buffers(max_block),
339 clip_b: buffers(max_block),
340 bus_in: buffers(max_block),
341 mix: buffers(max_block),
342 output: buffers(max_block),
343 envelope: [
344 vec![0.0; max_block],
345 vec![0.0; max_block],
346 vec![0.0; max_block],
347 ],
348 next_sample: None,
349 live_chains: Vec::new(),
350 blocks_mixed: 0,
351 weights: Vec::new(),
352 phase_weights: Vec::new(),
353 stretch: Stretch::new(sample_rate),
354 stretch_slots: HashMap::new(),
355 #[cfg(test)]
356 most_held: (0, 0),
357 #[cfg(test)]
358 samples_read: 0,
359 })
360 }
361
362 #[must_use]
364 pub const fn sample_rate(&self) -> u32 {
365 self.sample_rate
366 }
367
368 #[must_use]
370 pub const fn channels(&self) -> usize {
371 self.layout.channels()
372 }
373
374 #[must_use]
376 pub fn track_ids(&self) -> Vec<&TrackId> {
377 self.buses.iter().map(|bus| &bus.id).collect()
378 }
379
380 #[must_use]
382 pub const fn max_block(&self) -> usize {
383 self.max_block
384 }
385
386 #[must_use]
388 pub fn channel(&self, index: usize) -> &[f32] {
389 self.output.get(index).map_or(&[], Vec::as_slice)
390 }
391
392 #[must_use]
398 pub fn required_lookahead_samples(&self) -> u32 {
399 u32::try_from(self.lookahead).unwrap_or(u32::MAX)
400 }
401
402 pub fn audio_needs(
414 &self,
415 snapshot: &EngineProject,
416 start_sample: i64,
417 frames: usize,
418 ) -> Result<Vec<AudioNeed>, EngineError> {
419 self.check_block(start_sample, frames)?;
420 let mut needs = Vec::new();
421 let mut scratch = AudioSpanScratch::default();
422 if self.continues_at(start_sample).is_none() {
423 let mut position = self.preroll_start(start_sample)?;
424 while position < start_sample {
425 let count = self.replay_frames(position, start_sample);
426 self.block_needs(
427 snapshot,
428 position,
429 count,
430 Some(start_sample),
431 &mut scratch,
432 &mut needs,
433 )?;
434 position += i64::try_from(count).unwrap_or(i64::MAX);
435 }
436 }
437 self.block_needs(
438 snapshot,
439 start_sample,
440 frames,
441 None,
442 &mut scratch,
443 &mut needs,
444 )?;
445 Ok(needs)
446 }
447
448 fn block_needs(
450 &self,
451 snapshot: &EngineProject,
452 start: i64,
453 frames: usize,
454 replay_target: Option<i64>,
455 scratch: &mut AudioSpanScratch,
456 needs: &mut Vec<AudioNeed>,
457 ) -> Result<(), EngineError> {
458 let frames_i64 = i64::try_from(frames).map_err(|_| EngineError::OutOfRange)?;
459 let stages = Stages {
460 target: replay_target,
461 block_end: start + frames_i64,
462 lookahead: self.lookahead,
463 };
464 let span = self.block_span(start, frames_i64)?;
465 let described = snapshot
466 .evaluate_audio_in(span, scratch)
467 .map_err(EngineError::Evaluate)?;
468 let master_input = start + self.master_latency;
469 let rate = self.sample_rate;
470 for clip in described.iter() {
471 let Some(read) = self.clip_read(&clip, master_input, frames, stages) else {
472 continue;
473 };
474 source_requests(
475 &self.stretch,
476 &clip,
477 read.first,
478 read.window.content,
479 rate,
480 |from, to| {
481 let (Some(start), Some(end)) =
482 (checked_ticks_at(from, rate), ticks_at_or_after(to, rate))
483 else {
484 return;
485 };
486 if let Ok(range) =
487 TimeRange::new(MediaTime::from_ticks(start), MediaTime::from_ticks(end))
488 {
489 merge_need(needs, clip.source_id, clip.audio.stream, range);
490 }
491 },
492 );
493 }
494 Ok(())
495 }
496
497 fn check_block(&self, start_sample: i64, frames: usize) -> Result<(), EngineError> {
498 if start_sample < 0 || frames == 0 || frames > self.max_block {
499 return Err(EngineError::InvalidAudioConfiguration);
500 }
501 i64::try_from(frames)
503 .ok()
504 .and_then(|frames| start_sample.checked_add(frames))
505 .and_then(|end| end.checked_add(self.lookahead))
506 .and_then(|end| checked_ticks_at(end, self.sample_rate))
507 .map(|_| ())
508 .ok_or(EngineError::InvalidAudioConfiguration)
509 }
510
511 #[must_use]
514 pub const fn required_history_samples(&self) -> i64 {
515 self.history
516 }
517
518 fn preroll_start(&self, start: i64) -> Result<i64, EngineError> {
519 if start < 0 {
520 return Err(EngineError::InvalidAudioConfiguration);
521 }
522 let history = if self.continues_at(start).is_some() {
523 0
524 } else {
525 self.history
526 };
527 Ok(start
529 .saturating_sub(history)
530 .max(0)
531 .saturating_sub(self.lookahead))
532 }
533
534 fn replay_frames(&self, position: i64, target: i64) -> usize {
537 let room = i64::try_from(self.max_block).unwrap_or(i64::MAX);
538 let mut end = target.min(position.saturating_add(room));
539 let begun = self
541 .stage_leads
542 .partition_point(|&lead| Stages::start(target, lead, self.lookahead) <= position);
543 if let Some(&lead) = self.stage_leads.get(begun) {
544 end = end.min(Stages::start(target, lead, self.lookahead));
545 }
546 usize::try_from(end - position).unwrap_or(1)
547 }
548
549 fn continues_at(&self, start: i64) -> Option<i64> {
552 let step = start.checked_sub(self.next_sample?)?;
553 let room = i64::try_from(self.max_block).unwrap_or(i64::MAX);
554 let joins = self.continuity == AudioContinuity::ContinueSmallGaps && step.abs() <= room;
555 (step == 0 || joins).then_some(step)
556 }
557
558 fn step_live_chains(&mut self, step: i64) {
561 if step == 0 {
562 return;
563 }
564 for &(bus, position) in &self.live_chains {
565 if let Some(clip) = self.buses[bus].clips[position].chain.as_mut() {
566 clip.next_input = clip.next_input.and_then(|next| next.checked_add(step));
567 }
568 }
569 }
570
571 pub fn set_continuity(&mut self, continuity: AudioContinuity) {
574 if self.continuity != continuity {
575 self.reset();
576 self.continuity = continuity;
577 }
578 }
579
580 #[must_use]
582 pub const fn continuity(&self) -> AudioContinuity {
583 self.continuity
584 }
585
586 pub fn reset(&mut self) {
591 self.next_sample = None;
592 for &(bus, position) in &self.live_chains {
594 if let Some(clip) = self.buses[bus].clips[position].chain.as_mut() {
595 if let Some(chain) = clip.chain.as_mut() {
596 chain.reset();
597 }
598 clip.next_input = None;
599 }
600 }
601 for bus in &mut self.buses {
602 bus.chain.reset();
603 if let Some(ducking) = bus.ducker.as_mut() {
604 ducking.ducker.reset();
605 }
606 }
607 self.master_chain.reset();
608 if let Some(limiter) = self.limiter.as_mut() {
609 limiter.reset();
610 }
611 }
612
613 pub fn render(
629 &mut self,
630 snapshot: &EngineProject,
631 start_sample: i64,
632 frames: usize,
633 provider: &dyn AudioBlockProvider,
634 transport: TransportState,
635 ) -> Result<AudioRenderStatus, EngineError> {
636 self.check_block(start_sample, frames)?;
637 let end_sample = start_sample + i64::try_from(frames).unwrap_or(i64::MAX);
638 if self.hold_incomplete {
639 let missing_sources = self.unready_sources(snapshot, start_sample, frames, provider)?;
640 if !missing_sources.is_empty() {
641 for channel in 0..self.layout.channels() {
642 self.output[channel][..frames].fill(0.0);
643 }
644 return Ok(AudioRenderStatus {
645 start_sample,
646 frames,
647 missing_sources,
648 });
649 }
650 }
651 let mut missing_sources: Vec<SourceId> = Vec::new();
652 if let Some(step) = self.continues_at(start_sample) {
653 self.step_live_chains(step);
654 } else {
655 let mut position = self.preroll_start(start_sample)?;
656 self.reset();
657 while position < start_sample {
658 let count = self.replay_frames(position, start_sample);
659 self.mix_block(
660 snapshot,
661 position,
662 count,
663 provider,
664 transport,
665 Some(start_sample),
666 &mut missing_sources,
667 )?;
668 position += i64::try_from(count).unwrap_or(i64::MAX);
669 }
670 }
671 self.mix_block(
672 snapshot,
673 start_sample,
674 frames,
675 provider,
676 transport,
677 None,
678 &mut missing_sources,
679 )?;
680 self.next_sample = Some(end_sample);
681
682 Ok(AudioRenderStatus {
683 start_sample,
684 frames,
685 missing_sources,
686 })
687 }
688
689 fn unready_sources(
692 &mut self,
693 snapshot: &EngineProject,
694 start_sample: i64,
695 frames: usize,
696 provider: &dyn AudioBlockProvider,
697 ) -> Result<Vec<SourceId>, EngineError> {
698 let mut missing = Vec::new();
699 let mut position = if self.continues_at(start_sample).is_some() {
700 start_sample
701 } else {
702 self.preroll_start(start_sample)?
703 };
704 while position < start_sample {
705 let count = self.replay_frames(position, start_sample);
706 self.read_block(
707 snapshot,
708 position,
709 count,
710 provider,
711 Some(start_sample),
712 &mut missing,
713 )?;
714 position += i64::try_from(count).unwrap_or(i64::MAX);
715 }
716 self.read_block(snapshot, start_sample, frames, provider, None, &mut missing)?;
717 Ok(missing)
718 }
719
720 fn read_block(
722 &mut self,
723 snapshot: &EngineProject,
724 start: i64,
725 frames: usize,
726 provider: &dyn AudioBlockProvider,
727 replay_target: Option<i64>,
728 missing: &mut Vec<SourceId>,
729 ) -> Result<(), EngineError> {
730 let frames_i64 = i64::try_from(frames).map_err(|_| EngineError::OutOfRange)?;
731 let stages = Stages {
732 target: replay_target,
733 block_end: start + frames_i64,
734 lookahead: self.lookahead,
735 };
736 let span = self.block_span(start, frames_i64)?;
737 let mut scratch = std::mem::take(&mut self.span_scratch);
738 let described = match snapshot.evaluate_audio_in(span, &mut scratch) {
739 Ok(described) => described,
740 Err(error) => {
741 self.span_scratch = scratch;
742 return Err(EngineError::Evaluate(error));
743 }
744 };
745 let master_input = start + self.master_latency;
746 for clip in described.iter() {
747 let Some(read) = self.clip_read(&clip, master_input, frames, stages) else {
748 continue;
749 };
750 let window = read.window;
751 let complete = self.fill_clip(
752 &clip,
753 read.first,
754 window.count,
755 window.content,
756 provider,
757 Fill::Presence,
758 );
759 if !complete && !missing.contains(clip.source_id) {
760 missing.push(clip.source_id.clone());
761 }
762 }
763 self.span_scratch = scratch;
764 Ok(())
765 }
766
767 #[allow(clippy::too_many_arguments)]
774 fn mix_block(
775 &mut self,
776 snapshot: &EngineProject,
777 start: i64,
778 frames: usize,
779 provider: &dyn AudioBlockProvider,
780 transport: TransportState,
781 replay_target: Option<i64>,
782 missing_sources: &mut Vec<SourceId>,
783 ) -> Result<(), EngineError> {
784 let channels = self.layout.channels();
785 let frames_i64 = i64::try_from(frames).map_err(|_| EngineError::OutOfRange)?;
786 let stages = Stages {
787 target: replay_target,
788 block_end: start + frames_i64,
789 lookahead: self.lookahead,
790 };
791 let span = self.block_span(start, frames_i64)?;
792 let mut scratch = std::mem::take(&mut self.span_scratch);
794 let described = match snapshot.evaluate_audio_in(span, &mut scratch) {
795 Ok(described) => described,
796 Err(error) => {
797 self.span_scratch = scratch;
798 return Err(EngineError::Evaluate(error));
799 }
800 };
801
802 for channel in 0..channels {
803 self.mix[channel][..frames].fill(0.0);
804 }
805 for bus in &mut self.buses {
806 bus.live = false;
807 }
808
809 let master_input = start + self.master_latency;
810 self.keep_mixed_chains(&described, master_input, frames, stages);
811 let mut complete_block = true;
812 for clip in described.iter() {
813 let clip = &clip;
814 let Some(read) = self.clip_read(clip, master_input, frames, stages) else {
815 continue;
816 };
817 let window = read.window;
818 let complete = self.fill_clip(
819 clip,
820 read.first,
821 window.count,
822 window.content,
823 provider,
824 Fill::Samples,
825 );
826 complete_block &= complete;
827 if !complete && !missing_sources.contains(clip.source_id) {
828 missing_sources.push(clip.source_id.clone());
829 }
830 if let Some(slot) = read.slot {
831 self.apply_clip_chain(slot, read.first, transport, window.count);
832 }
833 self.mix_clip_into_bus(
834 clip,
835 read.bus,
836 read.bus_input,
837 window.offset,
838 window.count,
839 frames,
840 );
841 }
842 self.span_scratch = scratch;
843 let block = self.blocks_mixed;
845 self.stretch_slots.retain(|_, slot| slot.used_in == block);
846
847 self.process_buses(master_input, transport, frames, stages);
848 self.apply_ducking(&self.ctx(master_input, transport), frames, stages);
849 if stages.run(self.master_lead) {
850 self.sum_buses(master_input, frames);
851 self.finish_master(
852 start,
853 master_input,
854 transport,
855 frames,
856 stages,
857 complete_block,
858 );
859 }
860 self.blocks_mixed += 1;
861 Ok(())
862 }
863
864 fn keep_mixed_chains(
868 &mut self,
869 described: &alpha2_eval::AudioSpan<'_, '_>,
870 master_input: i64,
871 frames: usize,
872 stages: Stages,
873 ) {
874 let block = self.blocks_mixed;
875 for clip in described.iter() {
876 let Some((bus, position)) = self
877 .clip_read(&clip, master_input, frames, stages)
878 .and_then(|read| read.slot)
879 else {
880 continue;
881 };
882 if let Some(chain) = self.buses[bus].clips[position].chain.as_mut() {
883 chain.ran_in = block;
884 }
885 }
886 let buses = &mut self.buses;
887 self.live_chains.retain(|&(bus, position)| {
888 let Some(clip) = buses[bus].clips[position].chain.as_mut() else {
889 return false;
890 };
891 if clip.ran_in == block {
892 return true;
893 }
894 clip.chain = None;
895 clip.next_input = None;
896 false
897 });
898 }
899
900 fn ctx(&self, sample: i64, transport: TransportState) -> ProcessCtx {
901 ProcessCtx {
902 position: MediaTime::from_ticks(ticks_at(sample, self.sample_rate)),
903 sample_position: sample,
904 sample_rate: self.sample_rate,
905 transport,
906 }
907 }
908
909 pub fn set_normalization_gain_db(&mut self, gain_db: f64) -> Result<(), EngineError> {
916 #[allow(clippy::cast_possible_truncation)]
917 let gain = libm::pow(10.0, gain_db / 20.0) as f32;
918 if !gain_db.is_finite() || !gain.is_normal() {
919 return Err(EngineError::InvalidNormalizationGain);
920 }
921 self.normalization_gain = gain;
922 Ok(())
923 }
924
925 pub const fn set_hold_incomplete_blocks(&mut self, hold: bool) {
929 self.hold_incomplete = hold;
930 }
931
932 pub fn set_loudness_metering(&mut self, enabled: bool) -> Result<(), EngineError> {
941 if enabled {
942 self.normalization_gain = 1.0;
943 }
944 self.meter = if enabled {
945 Some(
946 LoudnessMeter::new(self.sample_rate, self.layout.channels())
947 .map_err(|_| EngineError::InvalidAudioConfiguration)?,
948 )
949 } else {
950 None
951 };
952 Ok(())
953 }
954
955 #[must_use]
957 pub fn loudness_report(&self) -> Option<LoudnessReport> {
958 self.meter.as_ref().map(LoudnessMeter::report)
959 }
960
961 #[must_use]
964 pub fn limiter_threshold(&self) -> Option<f64> {
965 self.master
966 .limiter
967 .map(|limiter| f64::from(limiter.threshold))
968 }
969
970 pub fn set_output_loudness_metering(&mut self, enabled: bool) -> Result<(), EngineError> {
977 self.output_meter = if enabled {
978 Some(
979 LoudnessMeter::new(self.sample_rate, self.layout.channels())
980 .map_err(|_| EngineError::InvalidAudioConfiguration)?,
981 )
982 } else {
983 None
984 };
985 Ok(())
986 }
987
988 #[must_use]
990 pub fn output_loudness_report(&self) -> Option<LoudnessReport> {
991 self.output_meter.as_ref().map(LoudnessMeter::report)
992 }
993
994 #[must_use]
996 pub fn block(&self, status: &AudioRenderStatus) -> AudioBlock {
997 AudioBlock {
998 start_sample: status.start_sample,
999 sample_rate: self.sample_rate,
1000 speakers: match self.layout {
1001 ChannelLayout::Mono => SpeakerLayout::MONO,
1002 ChannelLayout::Stereo => SpeakerLayout::STEREO,
1003 },
1004 channels: (0..self.layout.channels())
1005 .map(|channel| self.output[channel][..status.frames].to_vec())
1006 .collect(),
1007 }
1008 }
1009
1010 fn bus_of(&self, clip: &ActiveAudioClip) -> Option<usize> {
1012 match self.buses.get(clip.track_order) {
1013 Some(bus) if bus.id == *clip.track_id => Some(clip.track_order),
1014 _ => self.bus_of_track.get(clip.track_id).copied(),
1015 }
1016 }
1017
1018 fn slot_of(&self, clip: &ActiveAudioClip) -> Option<(usize, usize)> {
1020 let slot = (clip.track_order, clip.clip_order);
1021 let matches = self
1022 .buses
1023 .get(slot.0)
1024 .and_then(|bus| bus.clips.get(slot.1))
1025 .is_some_and(|found| found.id == *clip.clip_id);
1026 if matches {
1027 Some(slot)
1028 } else {
1029 self.slot_of_clip.get(clip.clip_id).copied()
1030 }
1031 }
1032
1033 fn clip_chain(&self, (bus, position): (usize, usize)) -> Option<&ClipChain> {
1034 self.buses[bus].clips[position].chain.as_ref()
1035 }
1036
1037 fn is_silent_bus(&self, index: usize) -> bool {
1039 self.buses[index].audio.is_silenced(self.any_solo)
1040 }
1041
1042 fn block_span(&self, start: i64, frames: i64) -> Result<TimeRange, EngineError> {
1045 let invalid = EngineError::InvalidAudioConfiguration;
1046 let span_end = start
1047 .checked_add(frames)
1048 .and_then(|end| end.checked_add(self.lookahead))
1049 .ok_or(invalid.clone())?;
1050 let start_ticks = checked_ticks_at(start, self.sample_rate).ok_or(invalid.clone())?;
1051 let end_ticks = checked_ticks_at(span_end, self.sample_rate)
1052 .zip(start_ticks.checked_add(1))
1053 .map(|(end, after_start)| end.max(after_start))
1054 .ok_or(invalid.clone())?;
1055 TimeRange::new(
1056 MediaTime::from_ticks(start_ticks),
1057 MediaTime::from_ticks(end_ticks),
1058 )
1059 .map_err(|_| invalid)
1060 }
1061
1062 fn clip_read(
1065 &self,
1066 clip: &ActiveAudioClip,
1067 master_input: i64,
1068 frames: usize,
1069 stages: Stages,
1070 ) -> Option<ClipRead> {
1071 let bus = self.bus_of(clip)?;
1072 if !clip.audio.is_audible() || !self.buses[bus].read {
1073 return None;
1074 }
1075 let bus_input = master_input + self.buses[bus].latency;
1076 let slot = self.slot_of(clip);
1077 let chain = slot.and_then(|slot| self.clip_chain(slot));
1078 let lead = chain.map_or(self.buses[bus].chain_lead, |chain| chain.lead);
1079 if !stages.run(lead) {
1080 return None;
1081 }
1082 let clip_latency = chain.map_or(0, |chain| chain.latency);
1083 let chain_input = bus_input + clip_latency;
1084 let window = self.clip_window(clip, chain_input, frames, clip_latency)?;
1085 Some(ClipRead {
1086 bus,
1087 bus_input,
1088 slot,
1089 first: chain_input + i64::try_from(window.offset).ok()?,
1090 window,
1091 })
1092 }
1093
1094 fn clip_window(
1097 &self,
1098 clip: &ActiveAudioClip,
1099 input: i64,
1100 frames: usize,
1101 latency: i64,
1102 ) -> Option<ClipWindow> {
1103 let first = sample_at_or_after(clip.timeline_range.start().ticks(), self.sample_rate);
1104 let last = sample_at_or_after(clip.timeline_range.end().ticks(), self.sample_rate);
1105 let block_end = input.checked_add(i64::try_from(frames).ok()?)?;
1106 let begin = first.max(input);
1107 let end = last.saturating_add(latency).min(block_end);
1108 if end <= begin {
1109 return None;
1110 }
1111 Some(ClipWindow {
1112 offset: usize::try_from(begin - input).ok()?,
1113 count: usize::try_from(end - begin).ok()?,
1114 content: usize::try_from((last.min(end) - begin).max(0)).ok()?,
1115 })
1116 }
1117
1118 fn fill_clip(
1121 &mut self,
1122 clip: &ActiveAudioClip,
1123 first_sample: i64,
1124 count: usize,
1125 content: usize,
1126 provider: &dyn AudioBlockProvider,
1127 fill: Fill,
1128 ) -> bool {
1129 if fill == Fill::Samples {
1130 for channel in 0..self.layout.channels() {
1131 self.clip_a[channel][..count].fill(0.0);
1132 }
1133 }
1134 let count = content;
1136 if count == 0 {
1137 return true;
1138 }
1139 let rate = self.sample_rate;
1140 match clip.time_map {
1141 TimeMap::Freeze { .. } => true,
1143 TimeMap::Forward | TimeMap::Reverse if stretches(clip) => {
1144 self.read_stretched(clip, first_sample, count, provider, fill)
1145 }
1146 TimeMap::Forward | TimeMap::Reverse => {
1147 let reversed = clip.time_map == TimeMap::Reverse;
1148 SourceMap::stretched(clip, reversed, rate).is_some_and(|map| {
1149 self.read_source(clip, &map, first_sample, 0, count, provider, fill)
1150 })
1151 }
1152 TimeMap::Loop { mode } => {
1153 self.fill_loop(clip, mode, first_sample, count, provider, fill)
1154 }
1155 }
1156 }
1157
1158 fn fill_loop(
1160 &mut self,
1161 clip: &ActiveAudioClip,
1162 mode: LoopMode,
1163 first_sample: i64,
1164 count: usize,
1165 provider: &dyn AudioBlockProvider,
1166 fill: Fill,
1167 ) -> bool {
1168 let rate = self.sample_rate;
1169 let end = clip.timeline_range.end().ticks();
1170 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
1171 let fade = ((f64::from(rate) * LOOP_FADE_SECONDS).round() as i64).max(1);
1172 let mut complete = true;
1173 let mapped = loop_pieces(clip, mode, first_sample, count, rate, |piece| {
1174 complete &= self.read_source(
1175 clip,
1176 &piece.map,
1177 piece.sample,
1178 piece.index,
1179 piece.length,
1180 provider,
1181 fill,
1182 );
1183 if mode == LoopMode::Repeat && fill == Fill::Samples {
1184 let fade_in = (piece.pass > 0).then(|| sample_at_or_after(piece.pass_start, rate));
1186 let fade_out = (piece.pass_end < end).then_some(piece.piece_end);
1187 self.fade_wraps(
1188 piece.sample,
1189 piece.index,
1190 piece.length,
1191 fade_in,
1192 fade_out,
1193 fade,
1194 );
1195 }
1196 });
1197 mapped && complete
1198 }
1199
1200 fn fade_wraps(
1203 &mut self,
1204 first_sample: i64,
1205 offset: usize,
1206 count: usize,
1207 fade_in: Option<i64>,
1208 fade_out: Option<i64>,
1209 fade: i64,
1210 ) {
1211 #[allow(clippy::cast_precision_loss)]
1212 let fade = fade as f64;
1213 for index in 0..count {
1214 let sample = first_sample + i64::try_from(index).unwrap_or(i64::MAX);
1215 #[allow(clippy::cast_precision_loss)]
1216 let after = fade_in.map_or(f64::INFINITY, |wrap| (sample - wrap) as f64 + 0.5);
1217 #[allow(clippy::cast_precision_loss)]
1218 let before = fade_out.map_or(f64::INFINITY, |wrap| (wrap - sample) as f64 - 0.5);
1219 let distance = after.min(before);
1220 if distance >= fade {
1221 continue;
1222 }
1223 #[allow(clippy::cast_possible_truncation)]
1224 let gain = (distance / fade).clamp(0.0, 1.0) as f32;
1225 for channel in 0..self.layout.channels() {
1226 self.clip_a[channel][offset + index] *= gain;
1227 }
1228 }
1229 }
1230
1231 #[allow(clippy::too_many_arguments)]
1234 fn read_source(
1235 &mut self,
1236 clip: &ActiveAudioClip,
1237 map: &SourceMap,
1238 first_sample: i64,
1239 offset: usize,
1240 count: usize,
1241 provider: &dyn AudioBlockProvider,
1242 fill: Fill,
1243 ) -> bool {
1244 let Some((request_start, request_end)) = map.request(first_sample, count) else {
1246 return false;
1247 };
1248 let request_frames =
1249 usize::try_from(request_end.saturating_sub(request_start)).unwrap_or(0);
1250 let Some(span) = provider.block(
1251 clip.source_id,
1252 clip.audio.stream,
1253 request_start,
1254 request_frames,
1255 ) else {
1256 return false;
1257 };
1258 if !span.covers(request_start, request_end) {
1260 return false;
1261 }
1262 let source = span.block;
1263 if source.sample_rate != self.sample_rate {
1264 return false;
1265 }
1266
1267 let Some(downmix) = clip_downmix(clip, source, self.layout) else {
1268 return false;
1269 };
1270 if fill == Fill::Presence {
1271 return true;
1272 }
1273 #[cfg(test)]
1274 {
1275 self.samples_read += count;
1276 }
1277 let read = SourceRead {
1278 planes: source.channels.as_slice(),
1279 downmix: &downmix,
1280 start: source.start_sample,
1281 offset,
1282 count,
1283 };
1284 match (map.cursor(first_sample), map.interpolator()) {
1285 (None, interpolator) => self.read_divided(&read, map, first_sample, interpolator),
1287 (Some(cursor), None) => self.read_copied(&read, cursor),
1288 (Some(cursor), Some(interpolator)) => {
1289 self.read_filtered(&read, map, cursor, interpolator);
1290 }
1291 }
1292 true
1293 }
1294
1295 fn read_copied(&mut self, read: &SourceRead<'_>, cursor: RationalCursor) {
1297 let Some(first) = cursor.whole.checked_sub(read.start) else {
1298 return;
1299 };
1300 for channel in 0..self.layout.channels() {
1301 let mix = read.downmix.channel(channel);
1302 let mut at = first;
1303 let outputs = &mut self.clip_a[channel][read.offset..read.offset + read.count];
1304 if let Some(plane) = mix.unity() {
1305 let plane = read.planes[plane].as_slice();
1306 for output in outputs {
1307 *output = copied(plane, at);
1308 at = at.wrapping_add(cursor.step_whole);
1309 }
1310 } else {
1311 for output in outputs {
1312 *output = mixed_sample(mix, read.planes, at);
1313 at = at.wrapping_add(cursor.step_whole);
1314 }
1315 }
1316 }
1317 }
1318
1319 fn read_filtered(
1322 &mut self,
1323 read: &SourceRead<'_>,
1324 map: &SourceMap,
1325 mut cursor: RationalCursor,
1326 interpolator: Interpolator,
1327 ) {
1328 let mut weights = std::mem::take(&mut self.weights);
1329 let reach = interpolator.half_taps() - 1;
1330 let taps = usize::try_from(2 * interpolator.half_taps()).unwrap_or(0);
1331 let phases = usize::try_from(cursor.period())
1332 .ok()
1333 .filter(|&period| period <= read.count / 2 && period * taps <= MAX_CACHED_WEIGHTS);
1334 if let Some(phases) = phases {
1335 weights.resize(phases * taps, 0.0);
1336 let mut scratch = std::mem::take(&mut self.phase_weights);
1337 let mut walk = cursor;
1338 for row in weights.chunks_exact_mut(taps) {
1339 interpolator.weights(map.fraction(&walk).1, &mut scratch);
1340 row.copy_from_slice(&scratch);
1341 walk.advance();
1342 }
1343 self.phase_weights = scratch;
1344 }
1345 let mut phase = 0;
1346 for index in 0..read.count {
1347 let (whole, fraction) = map.fraction(&cursor);
1348 cursor.advance();
1349 let row = if let Some(phases) = phases {
1350 let row = phase;
1351 phase = if phase + 1 == phases { 0 } else { phase + 1 };
1352 &weights[row * taps..(row + 1) * taps]
1353 } else {
1354 interpolator.weights(fraction, &mut weights);
1355 &weights[..]
1356 };
1357 let Some(first) = whole
1358 .checked_sub(read.start)
1359 .and_then(|at| at.checked_sub(reach))
1360 else {
1361 continue;
1362 };
1363 for channel in 0..self.layout.channels() {
1364 self.clip_a[channel][read.offset + index] =
1365 filtered_sample(read.downmix.channel(channel), read.planes, first, row);
1366 }
1367 }
1368 self.weights = weights;
1369 }
1370
1371 fn read_divided(
1373 &mut self,
1374 read: &SourceRead<'_>,
1375 map: &SourceMap,
1376 first_sample: i64,
1377 interpolator: Option<Interpolator>,
1378 ) {
1379 let mut weights = std::mem::take(&mut self.weights);
1380 for index in 0..read.count {
1381 let sample = first_sample + i64::try_from(index).unwrap_or(0);
1382 let Some((whole, fraction)) = map.position(sample) else {
1383 continue;
1384 };
1385 let Some(at) = whole.checked_sub(read.start) else {
1386 continue;
1387 };
1388 if let Some(interpolator) = interpolator {
1389 interpolator.weights(fraction, &mut weights);
1390 }
1391 for channel in 0..self.layout.channels() {
1392 let mix = read.downmix.channel(channel);
1393 self.clip_a[channel][read.offset + index] = match interpolator {
1394 None => mixed_sample(mix, read.planes, at),
1395 Some(interpolator) => filtered_sample(
1396 mix,
1397 read.planes,
1398 at - interpolator.half_taps() + 1,
1399 &weights,
1400 ),
1401 };
1402 }
1403 }
1404 self.weights = weights;
1405 }
1406
1407 fn apply_clip_chain(
1410 &mut self,
1411 (bus, position): (usize, usize),
1412 input: i64,
1413 transport: TransportState,
1414 count: usize,
1415 ) {
1416 let ctx = self.ctx(input, transport);
1417 let (layout, sample_rate, max_block) = (self.layout, self.sample_rate, self.max_block);
1418 let Some(clip_chain) = self.buses[bus].clips[position].chain.as_mut() else {
1419 return;
1420 };
1421 if clip_chain.chain.is_none() {
1422 self.live_chains.push((bus, position));
1423 }
1424 let effects = &clip_chain.effects;
1425 let chain = clip_chain
1426 .chain
1427 .get_or_insert_with(|| clip_processors(effects, layout, sample_rate, max_block));
1428 clip_chain.ran_in = self.blocks_mixed;
1429 if clip_chain.next_input != Some(input) {
1430 chain.reset();
1431 }
1432 clip_chain.next_input = Some(input + i64::try_from(count).unwrap_or(0));
1433 chain.process_in_place(&ctx, &mut self.clip_a, &mut self.clip_b, count);
1434 #[cfg(test)]
1435 self.note_held();
1436 }
1437
1438 #[cfg(test)]
1440 fn note_held(&mut self) {
1441 let held = self
1442 .live_chains
1443 .iter()
1444 .filter_map(|&(bus, position)| self.buses[bus].clips[position].chain.as_ref())
1445 .filter(|clip| clip.chain.is_some())
1446 .fold((0, 0), |(effects, delay), clip| {
1447 (
1448 effects + clip.effects.len(),
1449 delay
1450 + clip
1451 .effects
1452 .iter()
1453 .map(AudioEffect::delay_ticks)
1454 .sum::<u64>(),
1455 )
1456 });
1457 self.most_held = (self.most_held.0.max(held.0), self.most_held.1.max(held.1));
1458 }
1459
1460 fn mix_clip_into_bus(
1463 &mut self,
1464 clip: &ActiveAudioClip,
1465 bus_index: usize,
1466 start_sample: i64,
1467 offset: usize,
1468 count: usize,
1469 frames: usize,
1470 ) {
1471 let channels = self.layout.channels();
1472 let sample_rate = self.sample_rate;
1473 let bus = &mut self.buses[bus_index];
1474 let first = start_sample + i64::try_from(offset).unwrap_or(0);
1475 bus.make_live(channels, frames);
1476 let gain = constant_value(&clip.audio.gain).map(|gain| gain.clamp(0.0, MAX_GAIN));
1477 let weights =
1478 constant_value(&clip.audio.pan).map(|pan| pan_gains(pan.clamp(-1.0, 1.0), channels));
1479 if let (Some(gain), Some(weights), false) =
1480 (gain, weights, fades_reach(clip, first, count, sample_rate))
1481 {
1482 for (channel, weight) in weights.iter().enumerate().take(channels) {
1483 let input = &self.clip_a[channel][..count];
1484 let output = &mut bus.output[channel][offset..offset + count];
1485 for (output, sample) in output.iter_mut().zip(input) {
1486 *output += sample * gain * weight;
1487 }
1488 }
1489 return;
1490 }
1491 let audio = clip.audio;
1492 fill_envelope(
1493 &mut self.envelope[..],
1494 first,
1495 count,
1496 sample_rate,
1497 |ticks| {
1498 earliest(
1499 earliest(
1500 next_keyframe(&audio.gain, ticks),
1501 next_keyframe(&audio.pan, ticks),
1502 ),
1503 next_fade_edge(clip, ticks),
1504 )
1505 },
1506 |time| {
1507 let gain =
1508 sample_scalar(&audio.gain, time).clamp(0.0, MAX_GAIN) * fade_gain(clip, time);
1509 let [left, right] = pan_gains(sample_scalar(&audio.pan, time), channels);
1510 [gain, left, right]
1511 },
1512 );
1513 let [gains, left, right] = &self.envelope;
1514 for (channel, weights) in [left, right].into_iter().enumerate().take(channels) {
1515 let input = &self.clip_a[channel][..count];
1516 let output = &mut bus.output[channel][offset..offset + count];
1517 for (((output, sample), gain), weight) in
1518 output.iter_mut().zip(input).zip(gains).zip(weights)
1519 {
1520 *output += sample * gain * weight;
1521 }
1522 }
1523 }
1524
1525 fn process_buses(
1526 &mut self,
1527 master_input: i64,
1528 transport: TransportState,
1529 frames: usize,
1530 stages: Stages,
1531 ) {
1532 let channels = self.layout.channels();
1533 let sample_rate = self.sample_rate;
1534 for bus in &mut self.buses {
1535 if !stages.run(bus.chain_lead) || bus.chain.passes_through() {
1536 continue;
1537 }
1538 let input = master_input + bus.latency;
1539 let ctx = &ProcessCtx {
1540 position: MediaTime::from_ticks(ticks_at(input, sample_rate)),
1541 sample_position: input,
1542 sample_rate,
1543 transport,
1544 };
1545 bus.make_live(channels, frames);
1546 for channel in 0..channels {
1547 self.bus_in[channel][..frames].copy_from_slice(&bus.output[channel][..frames]);
1548 }
1549 match channels {
1550 1 => bus.chain.process(
1551 ctx,
1552 &[&self.bus_in[0][..frames]],
1553 &mut [&mut bus.output[0][..frames]],
1554 ),
1555 2 => {
1556 let (left, right) = bus.output.split_at_mut(1);
1557 bus.chain.process(
1558 ctx,
1559 &[&self.bus_in[0][..frames], &self.bus_in[1][..frames]],
1560 &mut [&mut left[0][..frames], &mut right[0][..frames]],
1561 );
1562 }
1563 _ => {}
1564 }
1565 }
1566 }
1567
1568 fn apply_ducking(&mut self, ctx: &ProcessCtx, frames: usize, stages: Stages) {
1573 let channels = self.layout.channels();
1574 let ducks = |bus: &TrackBus, index: usize| {
1575 bus.active_sidechain(index).is_some() && stages.run(bus.ducker_lead)
1576 };
1577 for index in 0..self.buses.len() {
1578 if !ducks(&self.buses[index], index) {
1579 continue;
1580 }
1581 let Some(sidechain) = self.buses[index].active_sidechain(index) else {
1582 continue;
1583 };
1584 self.buses[sidechain].make_live(channels, frames);
1586 for channel in 0..channels {
1587 self.bus_in[channel][..frames]
1588 .copy_from_slice(&self.buses[sidechain].output[channel][..frames]);
1589 }
1590 let bus = &mut self.buses[index];
1591 bus.make_live(channels, frames);
1592 let Some(BusDucker { ducker, output }) = bus.ducker.as_mut() else {
1593 continue;
1594 };
1595 match channels {
1596 1 => ducker.process(
1597 ctx,
1598 &[&bus.output[0][..frames], &self.bus_in[0][..frames]],
1599 &mut [&mut output[0][..frames]],
1600 ),
1601 2 => {
1602 let (left, right) = output.split_at_mut(1);
1603 ducker.process(
1604 ctx,
1605 &[
1606 &bus.output[0][..frames],
1607 &bus.output[1][..frames],
1608 &self.bus_in[0][..frames],
1609 &self.bus_in[1][..frames],
1610 ],
1611 &mut [&mut left[0][..frames], &mut right[0][..frames]],
1612 );
1613 }
1614 _ => {}
1615 }
1616 }
1617 for (index, bus) in self.buses.iter_mut().enumerate() {
1618 if ducks(bus, index)
1619 && let Some(ducking) = bus.ducker.as_mut()
1620 {
1621 std::mem::swap(&mut bus.output, &mut ducking.output);
1622 }
1623 }
1624 }
1625
1626 fn sum_buses(&mut self, start_sample: i64, frames: usize) {
1627 let channels = self.layout.channels();
1628 for bus_index in 0..self.buses.len() {
1629 if self.is_silent_bus(bus_index) || !self.buses[bus_index].live {
1631 continue;
1632 }
1633 let bus = &self.buses[bus_index];
1634 #[cfg(test)]
1635 BUS_PLANES_SUMMED.set(BUS_PLANES_SUMMED.get() + channels);
1636 let gain = constant_value(&bus.audio.gain).map(|gain| gain.clamp(0.0, MAX_GAIN));
1637 let weights =
1638 constant_value(&bus.audio.pan).map(|pan| pan_gains(pan.clamp(-1.0, 1.0), channels));
1639 if let (Some(gain), Some(weights)) = (gain, weights) {
1640 for (channel, weight) in weights.iter().enumerate().take(channels) {
1641 let input = &bus.output[channel][..frames];
1642 let output = &mut self.mix[channel][..frames];
1643 for (output, sample) in output.iter_mut().zip(input) {
1644 *output += sample * gain * weight;
1645 }
1646 }
1647 continue;
1648 }
1649 let audio = &bus.audio;
1650 fill_envelope(
1651 &mut self.envelope[..],
1652 start_sample,
1653 frames,
1654 self.sample_rate,
1655 |ticks| {
1656 earliest(
1657 next_keyframe(&audio.gain, ticks),
1658 next_keyframe(&audio.pan, ticks),
1659 )
1660 },
1661 |time| {
1662 let gain = sample_scalar(&audio.gain, time).clamp(0.0, MAX_GAIN);
1663 let [left, right] = pan_gains(sample_scalar(&audio.pan, time), channels);
1664 [gain, left, right]
1665 },
1666 );
1667 let [gains, left, right] = &self.envelope;
1668 for (channel, weights) in [left, right].into_iter().enumerate().take(channels) {
1669 let input = &bus.output[channel][..frames];
1670 let output = &mut self.mix[channel][..frames];
1671 for (((output, sample), gain), weight) in
1672 output.iter_mut().zip(input).zip(gains).zip(weights)
1673 {
1674 *output += sample * gain * weight;
1675 }
1676 }
1677 }
1678 }
1679
1680 fn finish_master(
1681 &mut self,
1682 start_sample: i64,
1683 master_input: i64,
1684 transport: TransportState,
1685 frames: usize,
1686 stages: Stages,
1687 complete: bool,
1688 ) {
1689 let channels = self.layout.channels();
1690 let input_ctx = self.ctx(master_input, transport);
1691 let ctx = &self.ctx(start_sample, transport);
1692 match channels {
1693 1 => self.master_chain.process(
1694 &input_ctx,
1695 &[&self.mix[0][..frames]],
1696 &mut [&mut self.output[0][..frames]],
1697 ),
1698 2 => {
1699 let (left, right) = self.output.split_at_mut(1);
1700 self.master_chain.process(
1701 &input_ctx,
1702 &[&self.mix[0][..frames], &self.mix[1][..frames]],
1703 &mut [&mut left[0][..frames], &mut right[0][..frames]],
1704 );
1705 }
1706 _ => {}
1707 }
1708 if let Some(gain) = constant_value(&self.master.gain) {
1709 let gain = gain.clamp(0.0, MAX_GAIN);
1710 for channel in 0..channels {
1711 for sample in &mut self.output[channel][..frames] {
1712 *sample *= gain;
1713 }
1714 }
1715 } else {
1716 let gain = &self.master.gain;
1717 fill_envelope(
1718 &mut self.envelope[..],
1719 start_sample,
1720 frames,
1721 self.sample_rate,
1722 |ticks| next_keyframe(gain, ticks),
1723 |time| [sample_scalar(gain, time).clamp(0.0, MAX_GAIN)],
1724 );
1725 for channel in 0..channels {
1726 let envelope = &self.envelope[0][..frames];
1727 for (sample, gain) in self.output[channel][..frames].iter_mut().zip(envelope) {
1728 *sample *= gain;
1729 }
1730 }
1731 }
1732 if self.normalization_gain.to_bits() != 1.0_f32.to_bits() {
1733 for channel in 0..channels {
1734 for sample in &mut self.output[channel][..frames] {
1735 *sample *= self.normalization_gain;
1736 }
1737 }
1738 }
1739 let measured = stages.target.is_none() && complete;
1741 if measured && let Some(meter) = self.meter.as_mut() {
1742 let planes = [&self.output[0][..frames], &self.output[1][..frames]];
1743 let _ = meter.push(&planes[..channels]);
1745 }
1746 if let Some(limiter) = self
1747 .limiter
1748 .as_mut()
1749 .filter(|_| stages.run(self.limiter_lead))
1750 {
1751 for channel in 0..channels {
1752 self.mix[channel][..frames].copy_from_slice(&self.output[channel][..frames]);
1753 }
1754 match channels {
1755 1 => limiter.process(
1756 ctx,
1757 &[&self.mix[0][..frames]],
1758 &mut [&mut self.output[0][..frames]],
1759 ),
1760 2 => {
1761 let (left, right) = self.output.split_at_mut(1);
1762 limiter.process(
1763 ctx,
1764 &[&self.mix[0][..frames], &self.mix[1][..frames]],
1765 &mut [&mut left[0][..frames], &mut right[0][..frames]],
1766 );
1767 }
1768 _ => {}
1769 }
1770 }
1771 if measured && let Some(meter) = self.output_meter.as_mut() {
1772 let planes = [&self.output[0][..frames], &self.output[1][..frames]];
1773 let _ = meter.push(&planes[..channels]);
1774 }
1775 }
1776}
1777
1778fn track_buses(
1779 project: &alpha2_model::Project,
1780 layout: ChannelLayout,
1781 sample_rate: u32,
1782 max_block: usize,
1783) -> (Vec<TrackBus>, HashMap<TrackId, usize>) {
1784 let mut buses = Vec::new();
1785 let mut bus_of_track = HashMap::new();
1786 for track in &project.timeline.tracks {
1788 let mut chain = ProcessorChain::new(layout);
1789 push_effects(&mut chain, &track.audio.effects);
1790 chain.prepare(sample_rate, max_block);
1791 let chain_latency = chain.latency_samples();
1792 let ducker = track.audio.ducking.as_ref().map(|ducking| {
1793 let mut ducker = Ducker::new(
1794 ducking.threshold,
1795 ducking.depth_db,
1796 ducking.attack_ms,
1797 ducking.release_ms,
1798 );
1799 ducker.prepare(sample_rate, max_block);
1800 BusDucker {
1801 ducker,
1802 output: buffers(max_block),
1803 }
1804 });
1805 bus_of_track.insert(track.id.clone(), buses.len());
1806 buses.push(TrackBus {
1807 id: track.id.clone(),
1808 audio: track.audio.clone(),
1809 chain,
1810 ducker,
1811 sidechain: None,
1812 output: buffers(max_block),
1813 live: false,
1814 latency: i64::from(chain_latency),
1815 clips: clip_slots(&track.clips, layout, sample_rate, max_block),
1816 chain_lead: 0,
1817 ducker_lead: 0,
1818 read: false,
1819 });
1820 }
1821 (buses, bus_of_track)
1822}
1823
1824fn clip_slots(
1826 clips: &[alpha2_model::Clip],
1827 layout: ChannelLayout,
1828 sample_rate: u32,
1829 max_block: usize,
1830) -> Vec<ClipSlot> {
1831 clips
1832 .iter()
1833 .map(|clip| ClipSlot {
1834 id: clip.id.clone(),
1835 chain: (!clip.audio.effects.is_empty()).then(|| {
1836 let chain = clip_processors(&clip.audio.effects, layout, sample_rate, max_block);
1838 ClipChain {
1839 effects: clip.audio.effects.clone(),
1840 chain: None,
1841 ran_in: 0,
1842 latency: i64::from(chain.latency_samples()),
1843 history: chain.history_samples(),
1844 next_input: None,
1845 lead: 0,
1846 }
1847 }),
1848 })
1849 .collect()
1850}
1851
1852fn clip_processors(
1853 effects: &[AudioEffect],
1854 layout: ChannelLayout,
1855 sample_rate: u32,
1856 max_block: usize,
1857) -> ProcessorChain {
1858 let mut chain = ProcessorChain::new(layout);
1859 push_effects(&mut chain, effects);
1860 chain.prepare_nodes(sample_rate, max_block);
1861 chain
1862}
1863
1864fn longest_path_latency(buses: &[TrackBus]) -> i64 {
1866 buses
1867 .iter()
1868 .map(|bus| {
1869 bus.latency
1870 + bus
1871 .clips
1872 .iter()
1873 .filter_map(|slot| slot.chain.as_ref())
1874 .map(|chain| chain.latency)
1875 .max()
1876 .unwrap_or(0)
1877 })
1878 .max()
1879 .unwrap_or(0)
1880}
1881
1882fn assign_leads(buses: &mut [TrackBus], master_lead: i64, settling: Settling) -> i64 {
1887 let ducker_leads: Vec<Option<i64>> = buses
1888 .iter()
1889 .enumerate()
1890 .map(|(index, bus)| {
1891 let ducking = bus.ducker.as_ref()?;
1892 bus.active_sidechain(index)?;
1893 Some(settling.lead(master_lead, ducking.ducker.history_samples()))
1894 })
1895 .collect();
1896 let mut longest = master_lead;
1897 for index in 0..buses.len() {
1898 let sidechained = buses
1899 .iter()
1900 .enumerate()
1901 .filter(|&(user, bus)| bus.active_sidechain(user) == Some(index))
1902 .filter_map(|(user, _)| ducker_leads[user]);
1903 let settled_from = sidechained
1904 .chain(ducker_leads[index])
1905 .fold(master_lead, i64::max);
1906 let bus = &mut buses[index];
1907 bus.ducker_lead = ducker_leads[index].unwrap_or(0);
1908 bus.chain_lead = settling.lead(settled_from, bus.chain.history_samples());
1909 longest = longest.max(bus.chain_lead);
1910 for clip in bus.clips.iter_mut().filter_map(|slot| slot.chain.as_mut()) {
1911 clip.lead = settling.lead(bus.chain_lead, clip.history);
1912 longest = longest.max(clip.lead);
1913 }
1914 }
1915 longest
1916}
1917
1918#[derive(Clone, Copy)]
1920struct Settling(i64);
1921
1922impl Settling {
1923 fn lead(self, downstream: i64, history: u32) -> i64 {
1926 downstream
1927 .saturating_add(i64::from(history))
1928 .min(self.0.max(downstream))
1929 }
1930}
1931
1932#[derive(Clone, Copy)]
1935struct Stages {
1936 target: Option<i64>,
1937 block_end: i64,
1938 lookahead: i64,
1939}
1940
1941impl Stages {
1942 fn run(self, lead: i64) -> bool {
1943 self.target
1944 .is_none_or(|target| self.block_end > Self::start(target, lead, self.lookahead))
1945 }
1946
1947 fn start(target: i64, lead: i64, lookahead: i64) -> i64 {
1950 (target - lead).max(0).saturating_sub(lookahead)
1951 }
1952}
1953
1954fn stage_leads(buses: &[TrackBus], master_lead: i64, limiter_lead: i64) -> Vec<i64> {
1956 let mut leads = vec![master_lead, limiter_lead];
1957 for (index, bus) in buses.iter().enumerate() {
1958 leads.push(bus.chain_lead);
1959 if bus.active_sidechain(index).is_some() {
1960 leads.push(bus.ducker_lead);
1961 }
1962 leads.extend(
1963 bus.clips
1964 .iter()
1965 .filter_map(|slot| slot.chain.as_ref())
1966 .map(|chain| chain.lead),
1967 );
1968 }
1969 leads.sort_unstable_by(|a, b| b.cmp(a));
1970 leads.dedup();
1971 leads
1972}
1973
1974fn read_buses(buses: &mut [TrackBus], any_solo: bool) {
1978 let heard: Vec<bool> = buses
1979 .iter()
1980 .map(|bus| !bus.audio.is_silenced(any_solo))
1981 .collect();
1982 let keys: Vec<Option<usize>> = buses
1983 .iter()
1984 .enumerate()
1985 .map(|(index, bus)| heard[index].then(|| bus.active_sidechain(index)).flatten())
1986 .collect();
1987 for (index, bus) in buses.iter_mut().enumerate() {
1988 bus.read = heard[index] || (!bus.audio.muted && keys.contains(&Some(index)));
1989 }
1990}
1991
1992fn resolve_sidechains(buses: &mut [TrackBus], bus_of_track: &HashMap<TrackId, usize>) {
1993 for bus in buses {
1994 bus.sidechain = bus
1995 .audio
1996 .ducking
1997 .as_ref()
1998 .and_then(|ducking| bus_of_track.get(&ducking.sidechain_track).copied());
1999 }
2000}
2001
2002fn buffers(max_block: usize) -> [Vec<f32>; 2] {
2003 [vec![0.0; max_block], vec![0.0; max_block]]
2004}
2005
2006fn push_effects(chain: &mut ProcessorChain, effects: &[AudioEffect]) {
2007 for effect in effects {
2008 match effect {
2009 AudioEffect::Eq { bands } => {
2010 for band in bands {
2011 push_effect(
2012 chain,
2013 Biquad::new(
2014 match band.kind {
2015 EqBandKind::Peaking => BiquadKind::Peaking,
2016 EqBandKind::LowPass => BiquadKind::LowPass,
2017 EqBandKind::HighPass => BiquadKind::HighPass,
2018 },
2019 band.frequency_hz,
2020 band.q,
2021 band.gain_db,
2022 ),
2023 );
2024 }
2025 }
2026 AudioEffect::Compressor {
2027 threshold_db,
2028 ratio,
2029 attack_ms,
2030 release_ms,
2031 lookahead_ms,
2032 } => push_effect(
2033 chain,
2034 Compressor::new(
2035 *threshold_db,
2036 *ratio,
2037 *attack_ms,
2038 *release_ms,
2039 *lookahead_ms,
2040 ),
2041 ),
2042 AudioEffect::Reverb {
2043 room_size,
2044 damping,
2045 mix,
2046 } => push_effect(chain, SchroederReverb::new(*room_size, *damping, *mix)),
2047 }
2048 }
2049}
2050
2051fn push_effect(chain: &mut ProcessorChain, effect: impl Processor + 'static) {
2052 chain
2053 .push(effect)
2054 .expect("a model effect reads one input per output");
2055}
2056
2057fn ticks_at(sample: i64, sample_rate: u32) -> i64 {
2058 checked_ticks_at(sample, sample_rate).unwrap_or(i64::MAX)
2059}
2060
2061fn checked_ticks_at(sample: i64, sample_rate: u32) -> Option<i64> {
2063 let rate = i128::from(sample_rate.max(1));
2064 i64::try_from((i128::from(sample) * i128::from(TICKS_PER_SECOND)).div_euclid(rate)).ok()
2065}
2066
2067struct SourceRead<'a> {
2069 planes: &'a [Vec<f32>],
2070 downmix: &'a Downmix,
2071 start: i64,
2073 offset: usize,
2074 count: usize,
2075}
2076
2077struct ClipRead {
2079 bus: usize,
2080 bus_input: i64,
2081 slot: Option<(usize, usize)>,
2082 first: i64,
2084 window: ClipWindow,
2085}
2086
2087#[derive(Clone, Copy)]
2089struct ClipWindow {
2090 offset: usize,
2092 count: usize,
2094 content: usize,
2096}
2097
2098fn ticks_at_or_after(sample: i64, sample_rate: u32) -> Option<i64> {
2100 let rate = i128::from(sample_rate.max(1));
2101 let numerator = i128::from(sample) * i128::from(TICKS_PER_SECOND);
2102 i64::try_from(numerator.div_euclid(rate) + i128::from(numerator.rem_euclid(rate) != 0)).ok()
2103}
2104
2105pub(crate) fn clip_downmix(
2109 clip: &ActiveAudioClip,
2110 source: &AudioBlock,
2111 layout: ChannelLayout,
2112) -> Option<Downmix> {
2113 if source.speakers.channels() != source.channels.len() {
2114 return None;
2115 }
2116 match clip.audio.channels.as_deref() {
2117 Some(selection) => Downmix::selected(selection, source.channels.len(), layout),
2118 None => Downmix::new(source.speakers.speakers(), layout),
2119 }
2120}
2121
2122fn merge_need(
2125 needs: &mut Vec<AudioNeed>,
2126 source_id: &SourceId,
2127 stream: Option<u32>,
2128 mut source_range: TimeRange,
2129) {
2130 while let Some(index) = needs.iter().position(|need| {
2132 need.source_id == *source_id
2133 && need.stream == stream
2134 && need.source_range.start() <= source_range.end()
2135 && source_range.start() <= need.source_range.end()
2136 }) {
2137 let existing = needs.remove(index).source_range;
2138 let merged_start = existing.start().min(source_range.start());
2139 let merged_end = existing.end().max(source_range.end());
2140 if let Ok(merged) = TimeRange::new(merged_start, merged_end) {
2141 source_range = merged;
2142 }
2143 }
2144 needs.push(AudioNeed {
2145 source_id: source_id.clone(),
2146 stream,
2147 source_range,
2148 });
2149}
2150
2151fn source_requests(
2154 stretch: &Stretch,
2155 clip: &ActiveAudioClip,
2156 first_sample: i64,
2157 count: usize,
2158 rate: u32,
2159 mut request: impl FnMut(i64, i64),
2160) {
2161 if count == 0 {
2162 return;
2163 }
2164 match clip.time_map {
2165 TimeMap::Freeze { .. } => {}
2166 TimeMap::Forward | TimeMap::Reverse if stretches(clip) => {
2167 if let Some((from, to)) = stretched_request(stretch, clip, first_sample, count, rate) {
2168 request(from, to);
2169 }
2170 }
2171 TimeMap::Forward | TimeMap::Reverse => {
2172 let reversed = clip.time_map == TimeMap::Reverse;
2173 if let Some((from, to)) = SourceMap::stretched(clip, reversed, rate)
2174 .and_then(|map| map.request(first_sample, count))
2175 {
2176 request(from, to);
2177 }
2178 }
2179 TimeMap::Loop { mode } => {
2180 loop_pieces(clip, mode, first_sample, count, rate, |piece| {
2181 if let Some((from, to)) = piece.map.request(piece.sample, piece.length) {
2182 request(from, to);
2183 }
2184 });
2185 }
2186 }
2187}
2188
2189struct LoopPiece {
2191 sample: i64,
2193 index: usize,
2194 length: usize,
2195 pass: i64,
2197 pass_start: i64,
2198 pass_end: i64,
2199 piece_end: i64,
2201 map: SourceMap,
2202}
2203
2204fn loop_pieces(
2207 clip: &ActiveAudioClip,
2208 mode: LoopMode,
2209 first_sample: i64,
2210 count: usize,
2211 rate: u32,
2212 mut piece: impl FnMut(LoopPiece),
2213) -> bool {
2214 let Some(period) = clip.source_range.duration().map(MediaTime::ticks) else {
2215 return false;
2216 };
2217 if period <= 0 {
2218 return false;
2219 }
2220 let start = clip.timeline_range.start().ticks();
2221 let mut index = 0;
2222 while index < count {
2223 let sample = first_sample + i64::try_from(index).unwrap_or(i64::MAX);
2224 let pass = (ticks_at(sample, rate) - start).div_euclid(period);
2225 let Some(pass_start) = pass
2226 .checked_mul(period)
2227 .and_then(|ticks| ticks.checked_add(start))
2228 else {
2229 return false;
2230 };
2231 let pass_end = pass_start.saturating_add(period);
2232 let piece_end = sample_at_or_after(pass_end, rate);
2233 let length = usize::try_from(piece_end.saturating_sub(sample))
2234 .unwrap_or(usize::MAX)
2235 .clamp(1, count - index);
2236 let reversed = mode == LoopMode::PingPong && pass.rem_euclid(2) == 1;
2237 let Some(map) = SourceMap::pass(clip, pass_start, reversed, rate) else {
2238 return false;
2239 };
2240 piece(LoopPiece {
2241 sample,
2242 index,
2243 length,
2244 pass,
2245 pass_start,
2246 pass_end,
2247 piece_end,
2248 map,
2249 });
2250 index += length;
2251 }
2252 true
2253}
2254
2255fn sample_at_or_after(ticks: i64, sample_rate: u32) -> i64 {
2256 let rate = i128::from(sample_rate.max(1));
2257 let numerator = i128::from(ticks) * rate;
2258 let denominator = i128::from(TICKS_PER_SECOND);
2259 let value =
2260 numerator.div_euclid(denominator) + i128::from(numerator.rem_euclid(denominator) != 0);
2261 i64::try_from(value).unwrap_or(i64::MAX)
2262}
2263
2264fn mixed_sample(mix: &ChannelMix, planes: &[Vec<f32>], index: i64) -> f32 {
2267 if let Some(plane) = mix.unity() {
2268 return copied(&planes[plane], index);
2269 }
2270 let mut sum = 0.0;
2271 for &(plane, weight) in mix.terms() {
2272 sum += weight * copied(&planes[plane], index);
2273 }
2274 sum
2275}
2276
2277fn filtered_sample(mix: &ChannelMix, planes: &[Vec<f32>], first: i64, weights: &[f64]) -> f32 {
2279 if let Some(plane) = mix.unity() {
2280 return filtered(&planes[plane], first, weights);
2281 }
2282 let mut sum = 0.0;
2283 for &(plane, weight) in mix.terms() {
2284 sum += weight * filtered(&planes[plane], first, weights);
2285 }
2286 sum
2287}
2288
2289fn copied(plane: &[f32], index: i64) -> f32 {
2292 let value = usize::try_from(index)
2293 .ok()
2294 .and_then(|index| plane.get(index).copied())
2295 .unwrap_or(0.0);
2296 if value.is_finite() { value } else { 0.0 }
2297}
2298
2299fn filtered(plane: &[f32], first: i64, weights: &[f64]) -> f32 {
2301 let mut sum = weighed(plane, first, weights, false);
2302 if !sum.is_finite() {
2303 sum = weighed(plane, first, weights, true);
2304 }
2305 #[allow(clippy::cast_possible_truncation)]
2306 let value = sum as f32;
2307 if value.is_finite() { value } else { 0.0 }
2308}
2309
2310fn weighed(plane: &[f32], first: i64, weights: &[f64], finite_only: bool) -> f64 {
2313 let inside = usize::try_from(first).ok().filter(|&from| {
2314 from.checked_add(weights.len())
2315 .is_some_and(|end| end <= plane.len())
2316 });
2317 let mut sums = [0.0_f64; 4];
2318 match inside {
2319 Some(from) if !finite_only => {
2320 let (weight_quads, weight_rest) = weights.as_chunks::<4>();
2321 let (sample_quads, sample_rest) = plane[from..from + weights.len()].as_chunks::<4>();
2322 for (weight, value) in weight_quads.iter().zip(sample_quads) {
2323 for lane in 0..4 {
2324 sums[lane] += weight[lane] * f64::from(value[lane]);
2325 }
2326 }
2327 for (lane, (weight, value)) in weight_rest.iter().zip(sample_rest).enumerate() {
2328 sums[lane] += weight * f64::from(*value);
2329 }
2330 }
2331 _ => {
2332 for (index, (at, weight)) in (first..).zip(weights).enumerate() {
2333 let value = usize::try_from(at)
2334 .ok()
2335 .and_then(|at| plane.get(at).copied())
2336 .unwrap_or(0.0);
2337 if value.is_finite() {
2338 sums[index % 4] += weight * f64::from(value);
2339 }
2340 }
2341 }
2342 }
2343 (sums[0] + sums[1]) + (sums[2] + sums[3])
2344}
2345
2346struct SourceMap {
2351 a: i128,
2352 b: i128,
2353 d: i128,
2354}
2355
2356impl SourceMap {
2357 fn stretched(clip: &ActiveAudioClip, reversed: bool, rate: u32) -> Option<Self> {
2360 let timeline = clip.timeline_range;
2361 let forward = Self::line(
2362 i128::from(timeline.start().ticks()),
2363 i128::from(clip.source_range.start().ticks()),
2364 i128::from(timeline.duration()?.ticks()),
2365 i128::from(clip.source_range.duration()?.ticks()),
2366 rate,
2367 )?;
2368 if !reversed {
2369 return Some(forward);
2370 }
2371 forward.mirrored(
2372 sample_at_or_after(timeline.start().ticks(), rate),
2373 sample_at_or_after(timeline.end().ticks(), rate),
2374 )
2375 }
2376
2377 fn pass(clip: &ActiveAudioClip, pass_start: i64, reversed: bool, rate: u32) -> Option<Self> {
2379 let period = clip.source_range.duration()?.ticks();
2380 let forward = Self::line(
2381 i128::from(pass_start),
2382 i128::from(clip.source_range.start().ticks()),
2383 i128::from(period),
2384 i128::from(period),
2385 rate,
2386 )?;
2387 if !reversed {
2388 return Some(forward);
2389 }
2390 forward.mirrored(
2391 sample_at_or_after(pass_start, rate),
2392 sample_at_or_after(pass_start.checked_add(period)?, rate),
2393 )
2394 }
2395
2396 fn mirrored(&self, first: i64, last: i64) -> Option<Self> {
2398 let pivot = i128::from(first)
2399 .checked_add(i128::from(last))?
2400 .checked_sub(1)?;
2401 Some(Self {
2402 a: self.a.checked_add(pivot.checked_mul(self.b)?)?,
2403 b: self.b.checked_neg()?,
2404 d: self.d,
2405 })
2406 }
2407
2408 fn line(t0: i128, s0: i128, timeline: i128, source: i128, rate: u32) -> Option<Self> {
2411 if timeline <= 0 || source <= 0 {
2412 return None;
2413 }
2414 let divisor = gcd(timeline, source);
2415 let (timeline, source) = (timeline / divisor, source / divisor);
2416 let rate = i128::from(rate);
2417 let ticks = i128::from(TICKS_PER_SECOND);
2418 let a = rate.checked_mul(
2420 s0.checked_mul(timeline)?
2421 .checked_sub(t0.checked_mul(source)?)?,
2422 )?;
2423 let b = ticks.checked_mul(source)?;
2424 let d = ticks.checked_mul(timeline)?;
2425 Some(Self { a, b, d })
2426 }
2427
2428 fn position(&self, sample: i64) -> Option<(i64, f64)> {
2430 let numerator = self
2431 .a
2432 .checked_add(i128::from(sample).checked_mul(self.b)?)?;
2433 let whole = i64::try_from(numerator.div_euclid(self.d)).ok()?;
2434 let remainder = numerator.rem_euclid(self.d);
2435 #[allow(clippy::cast_precision_loss)]
2436 let fraction = remainder as f64 / self.d as f64;
2437 Some((whole, fraction))
2438 }
2439
2440 fn interpolator(&self) -> Option<Interpolator> {
2442 if self.b.unsigned_abs() == self.d.unsigned_abs() && self.a.rem_euclid(self.d) == 0 {
2443 return None;
2444 }
2445 #[allow(clippy::cast_precision_loss)]
2446 let speed = self.b.unsigned_abs() as f64 / self.d.unsigned_abs() as f64;
2447 Some(Interpolator::new(speed))
2448 }
2449
2450 fn request(&self, first_sample: i64, count: usize) -> Option<(i64, i64)> {
2453 let last_sample = first_sample.checked_add(i64::try_from(count).ok()?.checked_sub(1)?)?;
2454 let (first, _) = self.position(first_sample)?;
2455 let (last, _) = self.position(last_sample)?;
2456 let (low, high) = (first.min(last), first.max(last));
2457 let Some(interpolator) = self.interpolator() else {
2458 return Some((low.max(0), high.saturating_add(1)));
2459 };
2460 let half = interpolator.half_taps();
2461 Some((
2462 low.saturating_sub(half - 1).max(0),
2463 high.saturating_add(half + 1),
2464 ))
2465 }
2466
2467 fn cursor(&self, sample: i64) -> Option<RationalCursor> {
2469 let numerator = self
2470 .a
2471 .checked_add(i128::from(sample).checked_mul(self.b)?)?;
2472 RationalCursor::new(numerator, self.b, self.d)
2473 }
2474
2475 fn fraction(&self, cursor: &RationalCursor) -> (i64, f64) {
2477 #[allow(clippy::cast_precision_loss)]
2479 let fraction = cursor.remainder as f64 / self.d as f64;
2480 (cursor.whole, fraction)
2481 }
2482}
2483
2484#[derive(Clone, Copy, Debug)]
2487struct RationalCursor {
2488 whole: i64,
2489 remainder: u64,
2490 step_whole: i64,
2491 step_remainder: u64,
2492 divisor: u64,
2493}
2494
2495impl RationalCursor {
2496 fn new(numerator: i128, step: i128, divisor: i128) -> Option<Self> {
2497 if divisor <= 0 || divisor > i128::from(i64::MAX) {
2499 return None;
2500 }
2501 Some(Self {
2502 whole: i64::try_from(numerator.div_euclid(divisor)).ok()?,
2503 remainder: u64::try_from(numerator.rem_euclid(divisor)).ok()?,
2504 step_whole: i64::try_from(step.div_euclid(divisor)).ok()?,
2505 step_remainder: u64::try_from(step.rem_euclid(divisor)).ok()?,
2506 divisor: u64::try_from(divisor).ok()?,
2507 })
2508 }
2509
2510 const fn period(&self) -> u64 {
2512 let (mut a, mut b) = (self.step_remainder, self.divisor);
2513 while b != 0 {
2514 (a, b) = (b, a % b);
2515 }
2516 self.divisor / a
2517 }
2518
2519 fn advance(&mut self) {
2520 self.whole = self.whole.wrapping_add(self.step_whole);
2521 self.remainder += self.step_remainder;
2522 if self.remainder >= self.divisor {
2523 self.remainder -= self.divisor;
2524 self.whole = self.whole.wrapping_add(1);
2525 }
2526 }
2527}
2528
2529fn tick_cursor(sample: i64, sample_rate: u32) -> RationalCursor {
2531 let ticks = i128::from(TICKS_PER_SECOND);
2532 RationalCursor::new(
2533 i128::from(sample) * ticks,
2534 ticks,
2535 i128::from(sample_rate.max(1)),
2536 )
2537 .unwrap_or(RationalCursor {
2539 whole: i64::MAX,
2540 remainder: 0,
2541 step_whole: 0,
2542 step_remainder: 0,
2543 divisor: 1,
2544 })
2545}
2546
2547fn gcd(mut a: i128, mut b: i128) -> i128 {
2548 while b != 0 {
2549 (a, b) = (b, a % b);
2550 }
2551 a.abs().max(1)
2552}
2553
2554const ENVELOPE_INTERVAL: i64 = 32;
2559
2560fn fill_envelope<const N: usize>(
2566 envelope: &mut [Vec<f32>],
2567 first: i64,
2568 count: usize,
2569 sample_rate: u32,
2570 next_knot: impl Fn(i64) -> Option<i64>,
2571 at: impl Fn(MediaTime) -> [f32; N],
2572) {
2573 #[allow(clippy::cast_precision_loss)]
2574 let fraction = 1.0 / ENVELOPE_INTERVAL as f32;
2575 let mut knot: Option<Option<i64>> = None;
2577 let mut point: Option<(i64, [f32; N])> = None;
2578 let mut written = 0;
2579 while written < count {
2580 let sample = first.saturating_add(i64::try_from(written).unwrap_or(i64::MAX));
2581 let start = sample.div_euclid(ENVELOPE_INTERVAL) * ENVELOPE_INTERVAL;
2582 let stop = start.saturating_add(ENVELOPE_INTERVAL);
2583 let run = usize::try_from(stop - sample)
2584 .unwrap_or(count)
2585 .clamp(1, count - written);
2586 let (from, to) = (ticks_at(start, sample_rate), ticks_at(stop, sample_rate));
2587 let next = match knot {
2588 Some(next) if next.is_none_or(|next| next > from) => next,
2589 _ => next_knot(from),
2590 };
2591 knot = Some(next);
2592 let outputs = written..written + run;
2593 if next.is_some_and(|next| next <= to) {
2594 let mut ticks = tick_cursor(sample, sample_rate);
2595 for index in outputs {
2596 let value = at(MediaTime::from_ticks(ticks.whole));
2597 ticks.advance();
2598 for (plane, value) in envelope.iter_mut().zip(value) {
2599 plane[index] = value;
2600 }
2601 }
2602 #[cfg(test)]
2603 ENVELOPE_POINTS.set(ENVELOPE_POINTS.get() + run);
2604 } else {
2605 let left = match point {
2606 Some((at_sample, value)) if at_sample == start => value,
2607 _ => {
2608 #[cfg(test)]
2609 ENVELOPE_POINTS.set(ENVELOPE_POINTS.get() + 1);
2610 at(MediaTime::from_ticks(from))
2611 }
2612 };
2613 let right = at(MediaTime::from_ticks(to));
2614 #[cfg(test)]
2615 ENVELOPE_POINTS.set(ENVELOPE_POINTS.get() + 1);
2616 point = Some((stop, right));
2617 #[allow(clippy::cast_precision_loss)]
2618 let along = (sample - start) as f32;
2619 for (plane, (left, right)) in envelope.iter_mut().zip(left.into_iter().zip(right)) {
2620 let slope = right - left;
2621 for (position, output) in plane[outputs.clone()].iter_mut().enumerate() {
2622 #[allow(clippy::cast_precision_loss)]
2623 let position = along + position as f32;
2624 *output = left + slope * (position * fraction);
2625 }
2626 }
2627 }
2628 written += run;
2629 }
2630}
2631
2632#[cfg(test)]
2633thread_local! {
2634 static ENVELOPE_POINTS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
2636 static BUS_PLANES_CLEARED: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
2638 static BUS_PLANES_SUMMED: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
2639}
2640
2641fn earliest(left: Option<i64>, right: Option<i64>) -> Option<i64> {
2643 match (left, right) {
2644 (Some(left), Some(right)) => Some(left.min(right)),
2645 (left, right) => left.or(right),
2646 }
2647}
2648
2649fn next_keyframe(animation: &Animated<f32>, ticks: i64) -> Option<i64> {
2651 match animation {
2652 Animated::Keyframes { keyframes } if keyframes.len() > 1 => {
2653 let next = keyframes.partition_point(|keyframe| keyframe.time.ticks() <= ticks);
2654 keyframes.get(next).map(|keyframe| keyframe.time.ticks())
2655 }
2656 _ => None,
2657 }
2658}
2659
2660fn next_fade_edge(clip: &ActiveAudioClip, ticks: i64) -> Option<i64> {
2662 let start = clip.timeline_range.start().ticks();
2663 let end = clip.timeline_range.end().ticks();
2664 let fade_in = clip.audio.fade_in.ticks();
2665 let fade_out = clip.audio.fade_out.ticks();
2666 let mut next = None;
2667 if fade_in > 0 {
2668 for edge in [start, start.saturating_add(fade_in)] {
2669 if edge > ticks {
2670 next = earliest(next, Some(edge));
2671 }
2672 }
2673 }
2674 if fade_out > 0 {
2675 for edge in [end.saturating_sub(fade_out), end] {
2676 if edge > ticks {
2677 next = earliest(next, Some(edge));
2678 }
2679 }
2680 }
2681 next
2682}
2683
2684fn constant_value(animation: &Animated<f32>) -> Option<f32> {
2686 match animation {
2687 Animated::Constant { value } => Some(*value),
2688 Animated::Keyframes { keyframes } => match keyframes.as_slice() {
2689 [only] => Some(only.value),
2690 _ => None,
2691 },
2692 }
2693}
2694
2695fn fades_reach(clip: &ActiveAudioClip, first: i64, count: usize, sample_rate: u32) -> bool {
2698 let fade_in = clip.audio.fade_in.ticks();
2699 let fade_out = clip.audio.fade_out.ticks();
2700 if fade_in <= 0 && fade_out <= 0 {
2701 return false;
2702 }
2703 let last = first.saturating_add(i64::try_from(count).unwrap_or(i64::MAX).saturating_sub(1));
2704 let elapsed = ticks_at(first, sample_rate).saturating_sub(clip.timeline_range.start().ticks());
2706 let remaining = clip
2707 .timeline_range
2708 .end()
2709 .ticks()
2710 .saturating_sub(ticks_at(last, sample_rate));
2711 (fade_in > 0 && elapsed < fade_in) || (fade_out > 0 && remaining < fade_out)
2712}
2713
2714fn fade_gain(clip: &ActiveAudioClip, time: MediaTime) -> f32 {
2716 let start = clip.timeline_range.start().ticks();
2717 let end = clip.timeline_range.end().ticks();
2718 let shape = clip.audio.fade_shape;
2719 let mut gain = 1.0_f32;
2720 let fade_in = clip.audio.fade_in.ticks();
2721 if fade_in > 0 {
2722 let elapsed = time.ticks().saturating_sub(start);
2723 if elapsed < fade_in {
2724 gain *= shape.amplitude(ratio(elapsed, fade_in));
2725 }
2726 }
2727 let fade_out = clip.audio.fade_out.ticks();
2728 if fade_out > 0 {
2729 let remaining = end.saturating_sub(time.ticks());
2730 if remaining < fade_out {
2731 gain *= shape.amplitude(ratio(remaining, fade_out));
2732 }
2733 }
2734 gain.clamp(0.0, 1.0)
2735}
2736
2737#[allow(clippy::cast_precision_loss)]
2738fn ratio(value: i64, span: i64) -> f32 {
2739 if span <= 0 {
2740 return 1.0;
2741 }
2742 (value.max(0) as f32 / span as f32).clamp(0.0, 1.0)
2743}
2744
2745fn pan_gains(pan: f32, channels: usize) -> [f32; 2] {
2749 if channels < 2 {
2750 return [1.0, 1.0];
2751 }
2752 let angle = (f64::from(pan.clamp(-1.0, 1.0)) + 1.0) * core::f64::consts::FRAC_PI_4;
2754 let normalize = core::f64::consts::SQRT_2;
2755 #[allow(clippy::cast_possible_truncation)]
2756 let weights = [
2757 (libm::cos(angle) * normalize) as f32,
2758 (libm::sin(angle) * normalize) as f32,
2759 ];
2760 weights
2761}
2762
2763#[cfg(test)]
2764mod tests;