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