Skip to main content

alpha2_engine/
audio.rs

1//! Project-driven audio mixing.
2//!
3//! [`AudioRenderer`] turns a project snapshot and host-decoded source blocks into one mixed,
4//! project-rate planar block. The host owns decode; this renderer owns gain, panning, fades,
5//! effects, bus summing, and limiting.
6//!
7//! Signal order per clip is fixed:
8//! source -> resample -> clip effects -> clip gain/fades -> pan -> track bus.
9//! Gain and fades come after the clip effects so a fade reaches true silence even when an effect
10//! has a tail.
11//!
12//! Track buses sum into the master bus. A ducked track reads the *pre-duck* output of its
13//! sidechain track, so the mix never depends on track order. A bus nothing writes in a block is
14//! neither cleared nor summed.
15
16use 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
38/// Cap on cached interpolation weights for a retimed read (256 KiB).
39const MAX_CACHED_WEIGHTS: usize = 1 << 15;
40
41/// Largest block a renderer will mix in one call.
42pub const MAX_AUDIO_BLOCK: usize = 8_192;
43
44// The model counts a clip's effects as built from a margin before its lookahead. That margin must
45// hold one largest block at the lowest rate plus the rounding at each clip end and chain lookahead.
46const _: () = {
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
55/// Longest a stage replays to settle before a seek target, in seconds.
56///
57/// A recursive effect's settling horizon is unbounded as it nears infinite memory, so each
58/// stage's lead is capped here. A seek replays at most this much audio plus the graph's lookahead;
59/// a stage with a longer horizon only approximates its settled state.
60pub const MAX_SETTLING_SECONDS: u32 = 30;
61
62/// What [`AudioRenderer::render`] does with a block that does not start
63/// where the previous one ended.
64#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
65pub enum AudioContinuity {
66    /// Every such block resets the renderer and replays the settling history before it, so it
67    /// mixes what a fresh renderer would. Use for exports, loudness measurement, and any output
68    /// that must repeat.
69    #[default]
70    Exact,
71    /// For live preview only. A block starting within [`AudioRenderer::max_block`] samples of
72    /// where the previous one ended continues from that state without a reset or replay, so a
73    /// small step costs one block instead of up to [`MAX_SETTLING_SECONDS`] of replay. The result
74    /// differs from a fresh renderer's but is still determined by the blocks asked for, in order.
75    /// Longer jumps, and the first block after a reset, are exact.
76    ContinueSmallGaps,
77}
78
79/// Outcome of mixing one block.
80#[derive(Clone, Debug, PartialEq, Eq)]
81pub struct AudioRenderStatus {
82    /// Sample index of the first mixed sample.
83    pub start_sample: i64,
84    /// Number of mixed samples per channel.
85    pub frames: usize,
86    /// Sources an audible clip needed but the provider could not supply.
87    ///
88    /// Those clips contributed silence and the block was not metered. Decode them and render the
89    /// block again. With [`AudioRenderer::set_hold_incomplete_blocks`] the whole block was silent,
90    /// nothing advanced, and the retry gives the uninterrupted mix; otherwise it gives the mix
91    /// after a seek.
92    pub missing_sources: Vec<SourceId>,
93}
94
95impl AudioRenderStatus {
96    /// Returns whether every audible clip was mixed from real samples.
97    #[must_use]
98    pub fn is_complete(&self) -> bool {
99        self.missing_sources.is_empty()
100    }
101}
102
103/// A source interval a host decodes before a render reads it.
104#[derive(Clone, Debug, PartialEq, Eq)]
105pub struct AudioNeed {
106    /// Source to decode or retrieve.
107    pub source_id: SourceId,
108    /// The source's audio stream, counted over its audio streams (`AudioProperties::stream`);
109    /// `None` is the host's default stream.
110    pub stream: Option<u32>,
111    /// Half-open source interval in ticks, including interpolation neighbours.
112    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    /// The bus's samples for the block, valid only while [`Self::live`].
122    output: [Vec<f32>; 2],
123    /// Whether `output` holds this block's samples. Otherwise it is stale and is cleared only when
124    /// a stage reads or adds to it ([`Self::make_live`]).
125    live: bool,
126    /// Samples the bus chain delays its input by.
127    latency: i64,
128    /// One slot per clip on the track, in the track's order.
129    clips: Vec<ClipSlot>,
130    /// How long before a seek target the bus chain must run to settle (see [`Leads`]).
131    chain_lead: i64,
132    /// The same for the ducker, when one runs.
133    ducker_lead: i64,
134    /// Whether the bus is heard or keys the ducker of one that is (see [`read_buses`]).
135    read: bool,
136}
137
138impl TrackBus {
139    /// The bus driving this bus's ducker, if any and not itself; `index` is this bus's own.
140    fn active_sidechain(&self, index: usize) -> Option<usize> {
141        self.ducker.as_ref()?;
142        self.sidechain.filter(|&sidechain| sidechain != index)
143    }
144
145    /// Clears the first `frames` samples of `output` unless something already wrote the block.
146    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
159/// A track's ducker and its output buffer, held only by buses that duck.
160struct BusDucker {
161    ducker: Ducker,
162    output: [Vec<f32>; 2],
163}
164
165struct ClipSlot {
166    id: ClipId,
167    chain: Option<ClipChain>,
168}
169
170/// A clip's own effect chain.
171///
172/// Processors exist only while the clip sounds (see [`AudioRenderer::keep_mixed_chains`]), so
173/// renderer state is bounded by the clips one block mixes
174/// ([`alpha2_model::Project::audio_effect_load`]), not by the whole timeline.
175struct ClipChain {
176    effects: Vec<AudioEffect>,
177    chain: Option<ProcessorChain>,
178    /// The block that last ran the chain.
179    ran_in: u64,
180    /// Samples the chain delays its input by.
181    latency: i64,
182    /// Samples the chain's nodes need to settle, excluding their delay.
183    history: u32,
184    /// Input position the chain continues from; anything else resets it.
185    next_input: Option<i64>,
186    /// How long before a seek target the chain must run to settle.
187    lead: i64,
188}
189
190/// Mixes a project's audio tracks into one project-rate block.
191pub struct AudioRenderer {
192    layout: ChannelLayout,
193    sample_rate: u32,
194    max_block: usize,
195    /// One bus per track, in project order.
196    buses: Vec<TrackBus>,
197    /// Buses by track ID, for clips evaluated from a different snapshot than the renderer's.
198    bus_of_track: HashMap<TrackId, usize>,
199    /// Clip slots by clip ID, `(bus, position)`, for the same case.
200    slot_of_clip: HashMap<ClipId, (usize, usize)>,
201    span_scratch: AudioSpanScratch,
202    master_chain: ProcessorChain,
203    /// Samples the master chain delays its input by.
204    master_latency: i64,
205    /// The longest latency from any clip's source to the output.
206    lookahead: i64,
207    /// Settling history beyond latency compensation: the longest lead of any stage.
208    history: i64,
209    /// How long before a seek target the master chain must run to settle.
210    master_lead: i64,
211    /// The same for the limiter.
212    limiter_lead: i64,
213    /// Distinct stage leads, longest first; a replay cuts its blocks at these so each stage starts
214    /// at its own sample whatever the block size (see [`Self::replay_frames`]).
215    stage_leads: Vec<i64>,
216    master: MasterAudio,
217    limiter: Option<PeakLimiter>,
218    /// Loudness normalization gain, between the master gain and the limiter. 1.0 unless set.
219    normalization_gain: f32,
220    hold_incomplete: bool,
221    continuity: AudioContinuity,
222    /// Measures what reaches the limiter, when metering is on.
223    meter: Option<LoudnessMeter>,
224    /// Measures the output after the limiter, when output metering is on.
225    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    /// Per-sample gain and left/right pan weights (see [`fill_envelope`]).
233    envelope: [Vec<f32>; 3],
234    /// Where the last successful render ended; `None` before the first.
235    next_sample: Option<i64>,
236    /// Slots, `(bus, position)`, whose clip chain is built.
237    live_chains: Vec<(usize, usize)>,
238    blocks_mixed: u64,
239    /// Interpolation weights for a retimed read, per position or per repeating fraction.
240    weights: Vec<f64>,
241    /// Scratch for one fraction's weights while `weights` holds them all.
242    phase_weights: Vec<f64>,
243    /// Pitch-preserving stretch state for clips a block reads.
244    stretch: Stretch,
245    stretch_slots: HashMap<ClipId, StretchSlot>,
246    /// The most clip effects, and their delay in ticks, built at once.
247    #[cfg(test)]
248    most_held: (usize, u64),
249    /// Project samples [`Self::read_source`] has read from sources.
250    #[cfg(test)]
251    samples_read: usize,
252}
253
254/// What [`AudioRenderer::fill_clip`] does with a clip's source samples.
255#[derive(Clone, Copy, Debug, PartialEq, Eq)]
256enum Fill {
257    /// Reads and resamples them into `clip_a`.
258    Samples,
259    /// Only checks that `provider` supplies them, as a held render does before mixing.
260    Presence,
261}
262
263impl AudioRenderer {
264    /// Builds every track and master processor for a project snapshot. Clip processors are built
265    /// when the clip is first mixed.
266    ///
267    /// # Errors
268    /// Returns [`EngineError::InvalidAudioConfiguration`] for an unsupported
269    /// channel count, a zero sample rate, or an oversized block.
270    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    /// Returns the project sample rate every block uses.
363    #[must_use]
364    pub const fn sample_rate(&self) -> u32 {
365        self.sample_rate
366    }
367
368    /// Returns the channel count of every mixed block.
369    #[must_use]
370    pub const fn channels(&self) -> usize {
371        self.layout.channels()
372    }
373
374    /// Returns the audio tracks this renderer mixes, in bus order.
375    #[must_use]
376    pub fn track_ids(&self) -> Vec<&TrackId> {
377        self.buses.iter().map(|bus| &bus.id).collect()
378    }
379
380    /// Returns the largest block this renderer accepts.
381    #[must_use]
382    pub const fn max_block(&self) -> usize {
383        self.max_block
384    }
385
386    /// Returns one mixed output channel from the most recent render.
387    #[must_use]
388    pub fn channel(&self, index: usize) -> &[f32] {
389        self.output.get(index).map_or(&[], Vec::as_slice)
390    }
391
392    /// How far ahead of a block, in samples, the renderer reads its sources.
393    ///
394    /// The renderer reads each path's input early by its latency to the output (for example a
395    /// compressor's `lookahead_ms`), so the output lines up with the timeline. Use
396    /// [`Self::audio_needs`] for source supply, since a cold seek also needs settling history.
397    #[must_use]
398    pub fn required_lookahead_samples(&self) -> u32 {
399        u32::try_from(self.lookahead).unwrap_or(u32::MAX)
400    }
401
402    /// The source intervals rendering `frames` samples from `start_sample` reads, for a host to
403    /// decode first.
404    ///
405    /// These are exactly what [`Self::render`] would read, including any settling replay, so a
406    /// host that holds them needs no margin. Overlapping or touching intervals of one source are
407    /// merged; disjoint ones stay separate.
408    ///
409    /// # Errors
410    /// Returns [`EngineError::InvalidAudioConfiguration`] for a negative start, an empty or
411    /// oversized block, or a span that does not fit in ticks, and an evaluation error when the
412    /// snapshot cannot be evaluated over a block's span.
413    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    /// Adds what one block of [`Self::mix_block`] reads to `needs`.
449    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        // Every later position lies inside this span, so nothing below can overflow.
502        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    /// Settling history replayed on discontinuities, in samples, excluding latency. At most
512    /// [`MAX_SETTLING_SECONDS`].
513    #[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        // Input before timeline zero is silent, so only the latency pre-roll reaches before it.
528        Ok(start
529            .saturating_sub(history)
530            .max(0)
531            .saturating_sub(self.lookahead))
532    }
533
534    /// Samples the replay before a seek to `target` mixes at `position`. Blocks are cut where a
535    /// stage starts, so the replay is the same for every block size.
536    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        // Leads are sorted longest first, so stage starts ascend.
540        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    /// The step from where the last block ended, when a block at `start` continues it. `None`
550    /// when it must reset and replay.
551    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    /// Shifts every built clip chain's next input by `step` so it carries its state across a
559    /// small step.
560    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    /// Sets the [`AudioContinuity`] policy (default [`AudioContinuity::Exact`]). Changing it resets
572    /// the renderer.
573    pub fn set_continuity(&mut self, continuity: AudioContinuity) {
574        if self.continuity != continuity {
575            self.reset();
576            self.continuity = continuity;
577        }
578    }
579
580    /// The policy [`Self::set_continuity`] last chose.
581    #[must_use]
582    pub const fn continuity(&self) -> AudioContinuity {
583        self.continuity
584    }
585
586    /// Clears every processor history without changing authored parameters.
587    ///
588    /// [`Self::render`] resets on its own at discontinuities. Call this only to start fresh exactly
589    /// where the last block ended, such as an export beginning where preview stopped.
590    pub fn reset(&mut self) {
591        self.next_sample = None;
592        // Built chains stay allocated; the next block drops the ones it does not mix.
593        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    /// Mixes `frames` samples starting at an absolute project-rate sample index.
614    ///
615    /// A block that does not continue the previous one (see [`AudioContinuity`]) first resets
616    /// every processor and replays the settling history [`Self::audio_needs`] asks for, so its
617    /// output matches a fresh renderer's regardless of block size.
618    ///
619    /// Recursive effects settle over a finite horizon (32 exponential time constants plus
620    /// filter/feedback margins, capped at [`MAX_SETTLING_SECONDS`]), not bit-exactly. Each stage
621    /// joins the replay only at its own horizon, so a long reverb on one track replays only that
622    /// track.
623    ///
624    /// # Errors
625    /// Returns [`EngineError::InvalidAudioConfiguration`] for a negative start, an oversized block,
626    /// or a span that does not fit in ticks, and an evaluation error when the snapshot cannot be
627    /// evaluated over the block's span.
628    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    /// Sources a render of `[start_sample, start_sample + frames)`, pre-roll included, would read
690    /// that `provider` lacks. Reads no samples and changes no renderer state.
691    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    /// Adds the sources `provider` lacks for one block of [`Self::mix_block`] to `missing`.
721    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    /// Mixes output samples `[start, start + frames)` into `output`, carrying processor state
768    /// forward.
769    ///
770    /// Each stage reads ahead by its latency to the output (master input at
771    /// `start + master_latency`, each bus and clip chain adding its own), so stage outputs line
772    /// up. A replay before a seek to `replay_target` runs only stages whose settling has begun.
773    #[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        // Take the scratch out of `self` so the clip loop can still borrow `self` mutably.
793        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        // Drop stretch readers this block did not use.
844        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    /// Drops the chain of every clip the block does not mix, before the block builds any, so only
865    /// one block's clips hold chains at once. A dropped clip mixed again would start from fresh
866    /// state anyway.
867    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    /// Sets the loudness normalization gain in dB, applied between the master gain and the
910    /// limiter. Zero removes it, as does [`Self::set_loudness_metering`].
911    ///
912    /// # Errors
913    /// Returns [`EngineError::InvalidNormalizationGain`] for a non-finite gain or one whose linear
914    /// factor is not a normal `f32` (beyond about 760 dB either way).
915    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    /// Whether a block missing a source is held: output as silence with nothing advanced, so
926    /// rendering it again after the host supplies the source matches an uninterrupted render.
927    /// Off by default (live playback); exports turn it on.
928    pub const fn set_hold_incomplete_blocks(&mut self, hold: bool) {
929        self.hold_incomplete = hold;
930    }
931
932    /// Starts (or restarts from empty) measuring the loudness of what reaches the limiter; `false`
933    /// stops. Only complete requested blocks are measured, never replayed history.
934    ///
935    /// Starting removes the normalization gain ([`Self::set_normalization_gain_db`]) so the
936    /// authored mix is measured; a gain set afterwards is included.
937    ///
938    /// # Errors
939    /// Returns [`EngineError::InvalidAudioConfiguration`] for an unsupported layout or rate.
940    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    /// What the meter has measured, if metering is on.
956    #[must_use]
957    pub fn loudness_report(&self) -> Option<LoudnessReport> {
958        self.meter.as_ref().map(LoudnessMeter::report)
959    }
960
961    /// The master limiter's linear threshold, if any. Pass it to
962    /// [`alpha2_audio::LoudnessNormalization::plan`] so the planned gain stops short of the limiter.
963    #[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    /// Starts (or restarts from empty) measuring the loudness of the final output, after the
971    /// limiter and normalization gain; `false` stops. Unlike [`Self::set_loudness_metering`] it
972    /// leaves the gain unchanged.
973    ///
974    /// # Errors
975    /// Returns [`EngineError::InvalidAudioConfiguration`] for an unsupported layout or rate.
976    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    /// What the output meter has measured, if output metering is on.
989    #[must_use]
990    pub fn output_loudness_report(&self) -> Option<LoudnessReport> {
991        self.output_meter.as_ref().map(LoudnessMeter::report)
992    }
993
994    /// Copies the most recent render into an owned block for offline sinks.
995    #[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    /// The bus of a clip's track, by track order or, for another snapshot's clip, by track ID.
1011    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    /// A clip's slot, `(bus, position)`, found as [`Self::bus_of`] finds its bus.
1019    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    /// Whether the bus is muted or outside the solo.
1038    fn is_silent_bus(&self, index: usize) -> bool {
1039        self.buses[index].audio.is_silenced(self.any_solo)
1040    }
1041
1042    /// The timeline span evaluated for output samples `[start, start + frames)`, including the
1043    /// lookahead.
1044    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    /// Where a block reads a clip, or `None` if it skips it. The mix and [`Self::audio_needs`]
1063    /// share this so a host is asked for exactly what the mix reads.
1064    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    /// The part of a block of chain input that a clip covers, plus `latency` samples of silence to
1095    /// flush its chain's delay line.
1096    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    /// Reads one clip's source window into `clip_a` (or only checks it, with [`Fill::Presence`]).
1119    /// Returns whether every requested sample came from supplied media.
1120    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        // Past the clip's end is the silence that flushes its chain.
1135        let count = content;
1136        if count == 0 {
1137            return true;
1138        }
1139        let rate = self.sample_rate;
1140        match clip.time_map {
1141            // A frozen clip is silent.
1142            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    /// Reads a looping clip one pass at a time. A repeating loop fades out and in around each wrap.
1159    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                // Fade around wraps inside the clip only; its own ends have authored fades.
1185                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    /// Applies a linear `fade`-sample ramp after the wrap at `fade_in` and before the one at
1201    /// `fade_out`, centred between the samples either side of the wrap.
1202    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    /// Reads `count` project samples from `first_sample` through `map` into `clip_a` at `offset`.
1232    /// Returns whether every requested sample came from supplied media.
1233    #[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        // Positions are exact rationals, so a source aligned with the project grid copies unchanged.
1245        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        // An uncovered part of the window is media that has not arrived, not silence.
1259        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            // The divisor does not fit the cursor, so divide per sample.
1286            (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    /// [`Self::read_source`] where every position lands on a whole source sample.
1296    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    /// [`Self::read_source`] through `interpolator`. When the fractional positions repeat with a
1320    /// short period (small speed ratios), each phase's weights are computed once per read.
1321    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    /// [`Self::read_source`] for a map whose divisor does not fit a cursor.
1372    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    /// Runs a clip's chain over `count` samples starting at `input`, resetting it unless it
1408    /// continues from where it stopped.
1409    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    /// Records the clip effects built now in [`Self::most_held`].
1439    #[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    /// Applies gain, fades, and panning, then sums into the bus. Constant gain and pan with no
1461    /// fade in the window are evaluated once; otherwise they follow [`fill_envelope`].
1462    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    /// Attenuates ducked buses from the pre-duck output of their sidechain.
1569    ///
1570    /// Every ducker writes its own buffer before any bus `output` changes, so a ducked sidechain
1571    /// is read pre-duck regardless of track order.
1572    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            // Either bus may be idle; the ducker still runs on silence.
1585            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            // Skipping an idle bus is exact: adding `+0.0` to a sum that is never `-0.0` is a no-op.
1630            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        // Meter only complete requested blocks, never replayed history or blocks missing a source.
1740        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            // The meter was made for this layout, so the block always fits.
1744            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    // Every track gets a bus, since a clip on a visual track can carry audio.
1787    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
1824/// One slot per clip. Chains are measured here and built when their clip is mixed.
1825fn 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                // Built once to measure latency and history, then dropped.
1837                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
1864/// The longest latency from any clip's input to its bus's output.
1865fn 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
1882/// Gives every bus, ducker, and clip chain its lead, and returns the longest.
1883///
1884/// A stage's lead is its own history plus the lead of the stage consuming its output. A bus that
1885/// feeds both the master sum and other buses' duckers takes the longest of their leads.
1886fn 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/// The lead cap in samples ([`MAX_SETTLING_SECONDS`]).
1919#[derive(Clone, Copy)]
1920struct Settling(i64);
1921
1922impl Settling {
1923    /// The lead of a stage with `history` feeding one with lead `downstream`. Never shorter than
1924    /// `downstream`, otherwise capped.
1925    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/// Which stages a block runs. In a replay before a seek to `target`, a stage runs once its lead
1933/// has begun; otherwise all run.
1934#[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    /// The output sample from which a stage with `lead` runs in the replay. As in
1948    /// `preroll_start`, history stops at timeline zero but the lookahead reaches before it.
1949    fn start(target: i64, lead: i64, lookahead: i64) -> i64 {
1950        (target - lead).max(0).saturating_sub(lookahead)
1951    }
1952}
1953
1954/// Every distinct stage lead, longest first.
1955fn 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
1974/// Marks the buses whose clips the mix reads: every heard bus and every unmuted bus keying the
1975/// ducker of a heard one. Solo changes what is heard, not how a ducked track sounds, so a soloed
1976/// track still reads its sidechain.
1977fn 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
2061/// The tick position of a sample, or `None` when it does not fit in ticks.
2062fn 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
2067/// A source block and where its samples go in `clip_a`.
2068struct SourceRead<'a> {
2069    planes: &'a [Vec<f32>],
2070    downmix: &'a Downmix,
2071    /// The source sample the planes start at.
2072    start: i64,
2073    offset: usize,
2074    count: usize,
2075}
2076
2077/// How one block reads one clip: see [`AudioRenderer::clip_read`].
2078struct ClipRead {
2079    bus: usize,
2080    bus_input: i64,
2081    slot: Option<(usize, usize)>,
2082    /// The project sample of the clip's first read sample.
2083    first: i64,
2084    window: ClipWindow,
2085}
2086
2087/// Where a clip lands in one block of its chain's input.
2088#[derive(Clone, Copy)]
2089struct ClipWindow {
2090    /// First block sample the clip covers.
2091    offset: usize,
2092    /// Samples the chain runs for, including the flush.
2093    count: usize,
2094    /// Of those, samples read from the source; the rest are silence.
2095    content: usize,
2096}
2097
2098/// The first tick at or after a sample, or `None` when it does not fit.
2099fn 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
2105/// How `clip` folds `source`'s planes into `layout`: its selected channels, or every plane by
2106/// speaker. The engine downmixes so the fold is the same on every platform. `None` when the block
2107/// is malformed or lacks a selected channel.
2108pub(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
2122/// Adds `source_range` to `needs`, merged with every need of that source stream it overlaps or
2123/// touches.
2124fn merge_need(
2125    needs: &mut Vec<AudioNeed>,
2126    source_id: &SourceId,
2127    stream: Option<u32>,
2128    mut source_range: TimeRange,
2129) {
2130    // A new range can bridge two existing needs, so keep absorbing until none touch.
2131    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
2151/// Calls `request` with each source window `[from, to)` that [`AudioRenderer::fill_clip`] would
2152/// read for the same arguments.
2153fn 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
2189/// One pass's share of a looping clip's read.
2190struct LoopPiece {
2191    /// The project sample the piece starts at; `index` is its offset into the read.
2192    sample: i64,
2193    index: usize,
2194    length: usize,
2195    /// The pass number; `pass_start` and `pass_end` are in timeline ticks.
2196    pass: i64,
2197    pass_start: i64,
2198    pass_end: i64,
2199    /// The first project sample of the next pass.
2200    piece_end: i64,
2201    map: SourceMap,
2202}
2203
2204/// Cuts a looping clip's read at its wraps and calls `piece` for each pass. Returns false if the
2205/// clip cannot be mapped.
2206fn 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
2264/// One project channel's sample at source sample `index`, folded by `mix`. Terms are summed in
2265/// plane order so the result is the same on every platform.
2266fn 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
2277/// Like [`mixed_sample`], filtering source samples `first..` by `weights`.
2278fn 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
2289/// Source sample `index` of `plane`. Samples outside the plane or non-finite are silence; reads
2290/// happen only where the block covers the request, so this matches explicit zeros.
2291fn 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
2299/// The weighted sum of `plane[first..first + weights.len()]`, treating samples as [`copied`] does.
2300fn 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
2310/// The sum behind [`filtered`]. Four interleaved running sums added pairwise give a fixed order
2311/// (identical on every platform) without one long dependency chain.
2312fn 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
2346/// Where each project sample of a clip reads its source, in source samples.
2347///
2348/// `p(n) = (a + n * b) / d` with integer terms, so positions are exact and independent of the
2349/// tick grid.
2350struct SourceMap {
2351    a: i128,
2352    b: i128,
2353    d: i128,
2354}
2355
2356impl SourceMap {
2357    /// A forward or reverse clip's timeline range stretched over its source range. Reversed reads
2358    /// the forward positions in the opposite order.
2359    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    /// One loop pass starting at timeline tick `pass_start`, at source speed.
2378    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    /// This map mirrored over `first..last`: sample `n` reads what `first + last - 1 - n` reads.
2397    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    /// The line through source tick `s0` at timeline tick `t0`, advancing
2409    /// `source` ticks per `timeline` ticks.
2410    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        // p(n) = (s0 + (n * TPS / rate - t0) * source / timeline) * rate / TPS
2419        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    /// The source sample at or before `sample`, and the fraction toward the next.
2429    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    /// The interpolation filter, or `None` at unity speed with every position on a source sample.
2441    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    /// The source window `[from, to)` that reading `count` samples from `first_sample` touches,
2451    /// including [`Interpolator`] taps. The map is linear, so the endpoints bound it.
2452    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    /// A cursor stepping one project sample at a time from `sample`, or `None` if it would not fit.
2468    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    /// What [`Self::position`] returns at the cursor's sample, bit for bit.
2476    fn fraction(&self, cursor: &RationalCursor) -> (i64, f64) {
2477        // The u64 remainder converts to f64 exactly as the i128 one would.
2478        #[allow(clippy::cast_precision_loss)]
2479        let fraction = cursor.remainder as f64 / self.d as f64;
2480        (cursor.whole, fraction)
2481    }
2482}
2483
2484/// Walks `(numerator + n * step) / divisor` over consecutive `n` with additions instead of 128-bit
2485/// divisions. The caller bounds the walk so every floor fits in `i64`.
2486#[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        // A divisor below 2^63 keeps `remainder + step_remainder` in a u64.
2498        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    /// Steps until the remainder repeats.
2511    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
2529/// [`ticks_at`] for consecutive samples, without a division per sample.
2530fn 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    // Unreachable for blocks `render` accepts; saturates like `ticks_at`.
2538    .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
2554/// Project samples between the points where automation is evaluated.
2555///
2556/// About 0.7 ms at 48 kHz: finer than any authored move, coarse enough that keyframe evaluation
2557/// (a cubic Bezier solve) stays cheap.
2558const ENVELOPE_INTERVAL: i64 = 32;
2559
2560/// Writes the `N` multipliers from `at` for `count` samples from `first` into `envelope`.
2561///
2562/// `at` is evaluated at multiples of [`ENVELOPE_INTERVAL`] and interpolated linearly between them.
2563/// An interval containing a knot from `next_knot` (keyframe or fade edge) is evaluated per sample
2564/// instead. Values depend only on the project sample, so block size and seeks never change them.
2565fn 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    // Outer `None` means not yet looked up; inner `None` means no further knot.
2576    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    /// How many times [`fill_envelope`] evaluated an envelope on this thread.
2635    static ENVELOPE_POINTS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
2636    /// Bus channels cleared and summed on this thread.
2637    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
2641/// The earlier of two optional ticks, where `None` means never.
2642fn 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
2649/// The first keyframe of `animation` after `ticks`; none for a constant or single keyframe.
2650fn 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
2660/// The first fade start or end after `ticks`.
2661fn 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
2684/// The value of a scalar that does not change over time, including a single keyframe.
2685fn 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
2695/// Whether either fade changes the gain in the `count` samples from `first`. Where it does not,
2696/// [`fade_gain`] is exactly one.
2697fn 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    // Saturate: pre-roll can read before the clip, and clips may end near the largest tick.
2705    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
2714/// The clip's fade multiplier at `time`, using its [`alpha2_model::FadeShape`].
2715fn 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
2745/// Constant-power pan weights normalized to unity at the centre, so clip and track pans compose
2746/// without attenuating a default project. A hard pan raises the remaining channel by 3 dB. Mono
2747/// output ignores pan.
2748fn pan_gains(pan: f32, channels: usize) -> [f32; 2] {
2749    if channels < 2 {
2750        return [1.0, 1.0];
2751    }
2752    // Computed in f64 so the centre rounds to exactly 1.0 in f32 (f32 math gives 0.99999994).
2753    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;