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    /// Half-open source interval in ticks, including interpolation neighbours.
109    pub source_range: TimeRange,
110}
111
112struct TrackBus {
113    id: TrackId,
114    audio: TrackAudio,
115    chain: ProcessorChain,
116    ducker: Option<BusDucker>,
117    sidechain: Option<usize>,
118    /// The bus's samples for the block, valid only while [`Self::live`].
119    output: [Vec<f32>; 2],
120    /// Whether `output` holds this block's samples. Otherwise it is stale and is cleared only when
121    /// a stage reads or adds to it ([`Self::make_live`]).
122    live: bool,
123    /// Samples the bus chain delays its input by.
124    latency: i64,
125    /// One slot per clip on the track, in the track's order.
126    clips: Vec<ClipSlot>,
127    /// How long before a seek target the bus chain must run to settle (see [`Leads`]).
128    chain_lead: i64,
129    /// The same for the ducker, when one runs.
130    ducker_lead: i64,
131    /// Whether the bus is heard or keys the ducker of one that is (see [`read_buses`]).
132    read: bool,
133}
134
135impl TrackBus {
136    /// The bus driving this bus's ducker, if any and not itself; `index` is this bus's own.
137    fn active_sidechain(&self, index: usize) -> Option<usize> {
138        self.ducker.as_ref()?;
139        self.sidechain.filter(|&sidechain| sidechain != index)
140    }
141
142    /// Clears the first `frames` samples of `output` unless something already wrote the block.
143    fn make_live(&mut self, channels: usize, frames: usize) {
144        if self.live {
145            return;
146        }
147        for plane in self.output.iter_mut().take(channels) {
148            plane[..frames].fill(0.0);
149        }
150        self.live = true;
151        #[cfg(test)]
152        BUS_PLANES_CLEARED.set(BUS_PLANES_CLEARED.get() + channels);
153    }
154}
155
156/// A track's ducker and its output buffer, held only by buses that duck.
157struct BusDucker {
158    ducker: Ducker,
159    output: [Vec<f32>; 2],
160}
161
162struct ClipSlot {
163    id: ClipId,
164    chain: Option<ClipChain>,
165}
166
167/// A clip's own effect chain.
168///
169/// Processors exist only while the clip sounds (see [`AudioRenderer::keep_mixed_chains`]), so
170/// renderer state is bounded by the clips one block mixes
171/// ([`alpha2_model::Project::audio_effect_load`]), not by the whole timeline.
172struct ClipChain {
173    effects: Vec<AudioEffect>,
174    chain: Option<ProcessorChain>,
175    /// The block that last ran the chain.
176    ran_in: u64,
177    /// Samples the chain delays its input by.
178    latency: i64,
179    /// Samples the chain's nodes need to settle, excluding their delay.
180    history: u32,
181    /// Input position the chain continues from; anything else resets it.
182    next_input: Option<i64>,
183    /// How long before a seek target the chain must run to settle.
184    lead: i64,
185}
186
187/// Mixes a project's audio tracks into one project-rate block.
188pub struct AudioRenderer {
189    layout: ChannelLayout,
190    sample_rate: u32,
191    max_block: usize,
192    /// One bus per track, in project order.
193    buses: Vec<TrackBus>,
194    /// Buses by track ID, for clips evaluated from a different snapshot than the renderer's.
195    bus_of_track: HashMap<TrackId, usize>,
196    /// Clip slots by clip ID, `(bus, position)`, for the same case.
197    slot_of_clip: HashMap<ClipId, (usize, usize)>,
198    span_scratch: AudioSpanScratch,
199    master_chain: ProcessorChain,
200    /// Samples the master chain delays its input by.
201    master_latency: i64,
202    /// The longest latency from any clip's source to the output.
203    lookahead: i64,
204    /// Settling history beyond latency compensation: the longest lead of any stage.
205    history: i64,
206    /// How long before a seek target the master chain must run to settle.
207    master_lead: i64,
208    /// The same for the limiter.
209    limiter_lead: i64,
210    /// Distinct stage leads, longest first; a replay cuts its blocks at these so each stage starts
211    /// at its own sample whatever the block size (see [`Self::replay_frames`]).
212    stage_leads: Vec<i64>,
213    master: MasterAudio,
214    limiter: Option<PeakLimiter>,
215    /// Loudness normalization gain, between the master gain and the limiter. 1.0 unless set.
216    normalization_gain: f32,
217    hold_incomplete: bool,
218    continuity: AudioContinuity,
219    /// Measures what reaches the limiter, when metering is on.
220    meter: Option<LoudnessMeter>,
221    /// Measures the output after the limiter, when output metering is on.
222    output_meter: Option<LoudnessMeter>,
223    any_solo: bool,
224    clip_a: [Vec<f32>; 2],
225    clip_b: [Vec<f32>; 2],
226    bus_in: [Vec<f32>; 2],
227    mix: [Vec<f32>; 2],
228    output: [Vec<f32>; 2],
229    /// Per-sample gain and left/right pan weights (see [`fill_envelope`]).
230    envelope: [Vec<f32>; 3],
231    /// Where the last successful render ended; `None` before the first.
232    next_sample: Option<i64>,
233    /// Slots, `(bus, position)`, whose clip chain is built.
234    live_chains: Vec<(usize, usize)>,
235    blocks_mixed: u64,
236    /// Interpolation weights for a retimed read, per position or per repeating fraction.
237    weights: Vec<f64>,
238    /// Scratch for one fraction's weights while `weights` holds them all.
239    phase_weights: Vec<f64>,
240    /// Pitch-preserving stretch state for clips a block reads.
241    stretch: Stretch,
242    stretch_slots: HashMap<ClipId, StretchSlot>,
243    /// The most clip effects, and their delay in ticks, built at once.
244    #[cfg(test)]
245    most_held: (usize, u64),
246    /// Project samples [`Self::read_source`] has read from sources.
247    #[cfg(test)]
248    samples_read: usize,
249}
250
251/// What [`AudioRenderer::fill_clip`] does with a clip's source samples.
252#[derive(Clone, Copy, Debug, PartialEq, Eq)]
253enum Fill {
254    /// Reads and resamples them into `clip_a`.
255    Samples,
256    /// Only checks that `provider` supplies them, as a held render does before mixing.
257    Presence,
258}
259
260impl AudioRenderer {
261    /// Builds every track and master processor for a project snapshot. Clip processors are built
262    /// when the clip is first mixed.
263    ///
264    /// # Errors
265    /// Returns [`EngineError::InvalidAudioConfiguration`] for an unsupported
266    /// channel count, a zero sample rate, or an oversized block.
267    pub fn new(
268        snapshot: &EngineProject,
269        channels: usize,
270        max_block: usize,
271    ) -> Result<Self, EngineError> {
272        let layout =
273            ChannelLayout::from_channels(channels).ok_or(EngineError::InvalidAudioConfiguration)?;
274        let project = snapshot.project();
275        let sample_rate = project.settings.sample_rate;
276        if sample_rate == 0 || max_block == 0 || max_block > MAX_AUDIO_BLOCK {
277            return Err(EngineError::InvalidAudioConfiguration);
278        }
279
280        let (mut buses, bus_of_track) = track_buses(project, layout, sample_rate, max_block);
281        resolve_sidechains(&mut buses, &bus_of_track);
282        let slot_of_clip = buses
283            .iter()
284            .enumerate()
285            .flat_map(|(bus, track)| {
286                track
287                    .clips
288                    .iter()
289                    .enumerate()
290                    .map(move |(position, slot)| (slot.id.clone(), (bus, position)))
291            })
292            .collect();
293
294        let mut master_chain = ProcessorChain::new(layout);
295        push_effects(&mut master_chain, &project.audio.effects);
296        master_chain.prepare(sample_rate, max_block);
297        let limiter = project.audio.limiter.map(|limiter| {
298            let mut peak = PeakLimiter::new(limiter.threshold, limiter.release_ms);
299            peak.prepare(sample_rate, max_block);
300            peak
301        });
302        let any_solo = project.timeline.any_solo();
303        read_buses(&mut buses, any_solo);
304        let master_latency = i64::from(master_chain.latency_samples());
305        let lookahead = master_latency + longest_path_latency(&buses);
306        let settling = Settling(i64::from(sample_rate) * i64::from(MAX_SETTLING_SECONDS));
307        let limiter_lead = settling.lead(0, limiter.as_ref().map_or(0, Processor::history_samples));
308        let master_lead = settling.lead(limiter_lead, master_chain.history_samples());
309        let history = assign_leads(&mut buses, master_lead, settling).max(master_lead);
310        let stage_leads = stage_leads(&buses, master_lead, limiter_lead);
311
312        Ok(Self {
313            layout,
314            sample_rate,
315            max_block,
316            buses,
317            bus_of_track,
318            slot_of_clip,
319            span_scratch: AudioSpanScratch::default(),
320            master_chain,
321            master_latency,
322            lookahead,
323            history,
324            master_lead,
325            limiter_lead,
326            stage_leads,
327            master: project.audio.clone(),
328            limiter,
329            normalization_gain: 1.0,
330            hold_incomplete: false,
331            continuity: AudioContinuity::Exact,
332            meter: None,
333            output_meter: None,
334            any_solo,
335            clip_a: buffers(max_block),
336            clip_b: buffers(max_block),
337            bus_in: buffers(max_block),
338            mix: buffers(max_block),
339            output: buffers(max_block),
340            envelope: [
341                vec![0.0; max_block],
342                vec![0.0; max_block],
343                vec![0.0; max_block],
344            ],
345            next_sample: None,
346            live_chains: Vec::new(),
347            blocks_mixed: 0,
348            weights: Vec::new(),
349            phase_weights: Vec::new(),
350            stretch: Stretch::new(sample_rate),
351            stretch_slots: HashMap::new(),
352            #[cfg(test)]
353            most_held: (0, 0),
354            #[cfg(test)]
355            samples_read: 0,
356        })
357    }
358
359    /// Returns the project sample rate every block uses.
360    #[must_use]
361    pub const fn sample_rate(&self) -> u32 {
362        self.sample_rate
363    }
364
365    /// Returns the channel count of every mixed block.
366    #[must_use]
367    pub const fn channels(&self) -> usize {
368        self.layout.channels()
369    }
370
371    /// Returns the audio tracks this renderer mixes, in bus order.
372    #[must_use]
373    pub fn track_ids(&self) -> Vec<&TrackId> {
374        self.buses.iter().map(|bus| &bus.id).collect()
375    }
376
377    /// Returns the largest block this renderer accepts.
378    #[must_use]
379    pub const fn max_block(&self) -> usize {
380        self.max_block
381    }
382
383    /// Returns one mixed output channel from the most recent render.
384    #[must_use]
385    pub fn channel(&self, index: usize) -> &[f32] {
386        self.output.get(index).map_or(&[], Vec::as_slice)
387    }
388
389    /// How far ahead of a block, in samples, the renderer reads its sources.
390    ///
391    /// The renderer reads each path's input early by its latency to the output (for example a
392    /// compressor's `lookahead_ms`), so the output lines up with the timeline. Use
393    /// [`Self::audio_needs`] for source supply, since a cold seek also needs settling history.
394    #[must_use]
395    pub fn required_lookahead_samples(&self) -> u32 {
396        u32::try_from(self.lookahead).unwrap_or(u32::MAX)
397    }
398
399    /// The source intervals rendering `frames` samples from `start_sample` reads, for a host to
400    /// decode first.
401    ///
402    /// These are exactly what [`Self::render`] would read, including any settling replay, so a
403    /// host that holds them needs no margin. Overlapping or touching intervals of one source are
404    /// merged; disjoint ones stay separate.
405    ///
406    /// # Errors
407    /// Returns [`EngineError::InvalidAudioConfiguration`] for a negative start, an empty or
408    /// oversized block, or a span that does not fit in ticks, and an evaluation error when the
409    /// snapshot cannot be evaluated over a block's span.
410    pub fn audio_needs(
411        &self,
412        snapshot: &EngineProject,
413        start_sample: i64,
414        frames: usize,
415    ) -> Result<Vec<AudioNeed>, EngineError> {
416        self.check_block(start_sample, frames)?;
417        let mut needs = Vec::new();
418        let mut scratch = AudioSpanScratch::default();
419        if self.continues_at(start_sample).is_none() {
420            let mut position = self.preroll_start(start_sample)?;
421            while position < start_sample {
422                let count = self.replay_frames(position, start_sample);
423                self.block_needs(
424                    snapshot,
425                    position,
426                    count,
427                    Some(start_sample),
428                    &mut scratch,
429                    &mut needs,
430                )?;
431                position += i64::try_from(count).unwrap_or(i64::MAX);
432            }
433        }
434        self.block_needs(
435            snapshot,
436            start_sample,
437            frames,
438            None,
439            &mut scratch,
440            &mut needs,
441        )?;
442        Ok(needs)
443    }
444
445    /// Adds what one block of [`Self::mix_block`] reads to `needs`.
446    fn block_needs(
447        &self,
448        snapshot: &EngineProject,
449        start: i64,
450        frames: usize,
451        replay_target: Option<i64>,
452        scratch: &mut AudioSpanScratch,
453        needs: &mut Vec<AudioNeed>,
454    ) -> Result<(), EngineError> {
455        let frames_i64 = i64::try_from(frames).map_err(|_| EngineError::OutOfRange)?;
456        let stages = Stages {
457            target: replay_target,
458            block_end: start + frames_i64,
459            lookahead: self.lookahead,
460        };
461        let span = self.block_span(start, frames_i64)?;
462        let described = snapshot
463            .evaluate_audio_in(span, scratch)
464            .map_err(EngineError::Evaluate)?;
465        let master_input = start + self.master_latency;
466        let rate = self.sample_rate;
467        for clip in described.iter() {
468            let Some(read) = self.clip_read(&clip, master_input, frames, stages) else {
469                continue;
470            };
471            source_requests(
472                &self.stretch,
473                &clip,
474                read.first,
475                read.window.content,
476                rate,
477                |from, to| {
478                    let (Some(start), Some(end)) =
479                        (checked_ticks_at(from, rate), ticks_at_or_after(to, rate))
480                    else {
481                        return;
482                    };
483                    if let Ok(range) =
484                        TimeRange::new(MediaTime::from_ticks(start), MediaTime::from_ticks(end))
485                    {
486                        merge_need(needs, clip.source_id, range);
487                    }
488                },
489            );
490        }
491        Ok(())
492    }
493
494    fn check_block(&self, start_sample: i64, frames: usize) -> Result<(), EngineError> {
495        if start_sample < 0 || frames == 0 || frames > self.max_block {
496            return Err(EngineError::InvalidAudioConfiguration);
497        }
498        // Every later position lies inside this span, so nothing below can overflow.
499        i64::try_from(frames)
500            .ok()
501            .and_then(|frames| start_sample.checked_add(frames))
502            .and_then(|end| end.checked_add(self.lookahead))
503            .and_then(|end| checked_ticks_at(end, self.sample_rate))
504            .map(|_| ())
505            .ok_or(EngineError::InvalidAudioConfiguration)
506    }
507
508    /// Settling history replayed on discontinuities, in samples, excluding latency. At most
509    /// [`MAX_SETTLING_SECONDS`].
510    #[must_use]
511    pub const fn required_history_samples(&self) -> i64 {
512        self.history
513    }
514
515    fn preroll_start(&self, start: i64) -> Result<i64, EngineError> {
516        if start < 0 {
517            return Err(EngineError::InvalidAudioConfiguration);
518        }
519        let history = if self.continues_at(start).is_some() {
520            0
521        } else {
522            self.history
523        };
524        // Input before timeline zero is silent, so only the latency pre-roll reaches before it.
525        Ok(start
526            .saturating_sub(history)
527            .max(0)
528            .saturating_sub(self.lookahead))
529    }
530
531    /// Samples the replay before a seek to `target` mixes at `position`. Blocks are cut where a
532    /// stage starts, so the replay is the same for every block size.
533    fn replay_frames(&self, position: i64, target: i64) -> usize {
534        let room = i64::try_from(self.max_block).unwrap_or(i64::MAX);
535        let mut end = target.min(position.saturating_add(room));
536        // Leads are sorted longest first, so stage starts ascend.
537        let begun = self
538            .stage_leads
539            .partition_point(|&lead| Stages::start(target, lead, self.lookahead) <= position);
540        if let Some(&lead) = self.stage_leads.get(begun) {
541            end = end.min(Stages::start(target, lead, self.lookahead));
542        }
543        usize::try_from(end - position).unwrap_or(1)
544    }
545
546    /// The step from where the last block ended, when a block at `start` continues it. `None`
547    /// when it must reset and replay.
548    fn continues_at(&self, start: i64) -> Option<i64> {
549        let step = start.checked_sub(self.next_sample?)?;
550        let room = i64::try_from(self.max_block).unwrap_or(i64::MAX);
551        let joins = self.continuity == AudioContinuity::ContinueSmallGaps && step.abs() <= room;
552        (step == 0 || joins).then_some(step)
553    }
554
555    /// Shifts every built clip chain's next input by `step` so it carries its state across a
556    /// small step.
557    fn step_live_chains(&mut self, step: i64) {
558        if step == 0 {
559            return;
560        }
561        for &(bus, position) in &self.live_chains {
562            if let Some(clip) = self.buses[bus].clips[position].chain.as_mut() {
563                clip.next_input = clip.next_input.and_then(|next| next.checked_add(step));
564            }
565        }
566    }
567
568    /// Sets the [`AudioContinuity`] policy (default [`AudioContinuity::Exact`]). Changing it resets
569    /// the renderer.
570    pub fn set_continuity(&mut self, continuity: AudioContinuity) {
571        if self.continuity != continuity {
572            self.reset();
573            self.continuity = continuity;
574        }
575    }
576
577    /// The policy [`Self::set_continuity`] last chose.
578    #[must_use]
579    pub const fn continuity(&self) -> AudioContinuity {
580        self.continuity
581    }
582
583    /// Clears every processor history without changing authored parameters.
584    ///
585    /// [`Self::render`] resets on its own at discontinuities. Call this only to start fresh exactly
586    /// where the last block ended, such as an export beginning where preview stopped.
587    pub fn reset(&mut self) {
588        self.next_sample = None;
589        // Built chains stay allocated; the next block drops the ones it does not mix.
590        for &(bus, position) in &self.live_chains {
591            if let Some(clip) = self.buses[bus].clips[position].chain.as_mut() {
592                if let Some(chain) = clip.chain.as_mut() {
593                    chain.reset();
594                }
595                clip.next_input = None;
596            }
597        }
598        for bus in &mut self.buses {
599            bus.chain.reset();
600            if let Some(ducking) = bus.ducker.as_mut() {
601                ducking.ducker.reset();
602            }
603        }
604        self.master_chain.reset();
605        if let Some(limiter) = self.limiter.as_mut() {
606            limiter.reset();
607        }
608    }
609
610    /// Mixes `frames` samples starting at an absolute project-rate sample index.
611    ///
612    /// A block that does not continue the previous one (see [`AudioContinuity`]) first resets
613    /// every processor and replays the settling history [`Self::audio_needs`] asks for, so its
614    /// output matches a fresh renderer's regardless of block size.
615    ///
616    /// Recursive effects settle over a finite horizon (32 exponential time constants plus
617    /// filter/feedback margins, capped at [`MAX_SETTLING_SECONDS`]), not bit-exactly. Each stage
618    /// joins the replay only at its own horizon, so a long reverb on one track replays only that
619    /// track.
620    ///
621    /// # Errors
622    /// Returns [`EngineError::InvalidAudioConfiguration`] for a negative start, an oversized block,
623    /// or a span that does not fit in ticks, and an evaluation error when the snapshot cannot be
624    /// evaluated over the block's span.
625    pub fn render(
626        &mut self,
627        snapshot: &EngineProject,
628        start_sample: i64,
629        frames: usize,
630        provider: &dyn AudioBlockProvider,
631        transport: TransportState,
632    ) -> Result<AudioRenderStatus, EngineError> {
633        self.check_block(start_sample, frames)?;
634        let end_sample = start_sample + i64::try_from(frames).unwrap_or(i64::MAX);
635        if self.hold_incomplete {
636            let missing_sources = self.unready_sources(snapshot, start_sample, frames, provider)?;
637            if !missing_sources.is_empty() {
638                for channel in 0..self.layout.channels() {
639                    self.output[channel][..frames].fill(0.0);
640                }
641                return Ok(AudioRenderStatus {
642                    start_sample,
643                    frames,
644                    missing_sources,
645                });
646            }
647        }
648        let mut missing_sources: Vec<SourceId> = Vec::new();
649        if let Some(step) = self.continues_at(start_sample) {
650            self.step_live_chains(step);
651        } else {
652            let mut position = self.preroll_start(start_sample)?;
653            self.reset();
654            while position < start_sample {
655                let count = self.replay_frames(position, start_sample);
656                self.mix_block(
657                    snapshot,
658                    position,
659                    count,
660                    provider,
661                    transport,
662                    Some(start_sample),
663                    &mut missing_sources,
664                )?;
665                position += i64::try_from(count).unwrap_or(i64::MAX);
666            }
667        }
668        self.mix_block(
669            snapshot,
670            start_sample,
671            frames,
672            provider,
673            transport,
674            None,
675            &mut missing_sources,
676        )?;
677        self.next_sample = Some(end_sample);
678
679        Ok(AudioRenderStatus {
680            start_sample,
681            frames,
682            missing_sources,
683        })
684    }
685
686    /// Sources a render of `[start_sample, start_sample + frames)`, pre-roll included, would read
687    /// that `provider` lacks. Reads no samples and changes no renderer state.
688    fn unready_sources(
689        &mut self,
690        snapshot: &EngineProject,
691        start_sample: i64,
692        frames: usize,
693        provider: &dyn AudioBlockProvider,
694    ) -> Result<Vec<SourceId>, EngineError> {
695        let mut missing = Vec::new();
696        let mut position = if self.continues_at(start_sample).is_some() {
697            start_sample
698        } else {
699            self.preroll_start(start_sample)?
700        };
701        while position < start_sample {
702            let count = self.replay_frames(position, start_sample);
703            self.read_block(
704                snapshot,
705                position,
706                count,
707                provider,
708                Some(start_sample),
709                &mut missing,
710            )?;
711            position += i64::try_from(count).unwrap_or(i64::MAX);
712        }
713        self.read_block(snapshot, start_sample, frames, provider, None, &mut missing)?;
714        Ok(missing)
715    }
716
717    /// Adds the sources `provider` lacks for one block of [`Self::mix_block`] to `missing`.
718    fn read_block(
719        &mut self,
720        snapshot: &EngineProject,
721        start: i64,
722        frames: usize,
723        provider: &dyn AudioBlockProvider,
724        replay_target: Option<i64>,
725        missing: &mut Vec<SourceId>,
726    ) -> Result<(), EngineError> {
727        let frames_i64 = i64::try_from(frames).map_err(|_| EngineError::OutOfRange)?;
728        let stages = Stages {
729            target: replay_target,
730            block_end: start + frames_i64,
731            lookahead: self.lookahead,
732        };
733        let span = self.block_span(start, frames_i64)?;
734        let mut scratch = std::mem::take(&mut self.span_scratch);
735        let described = match snapshot.evaluate_audio_in(span, &mut scratch) {
736            Ok(described) => described,
737            Err(error) => {
738                self.span_scratch = scratch;
739                return Err(EngineError::Evaluate(error));
740            }
741        };
742        let master_input = start + self.master_latency;
743        for clip in described.iter() {
744            let Some(read) = self.clip_read(&clip, master_input, frames, stages) else {
745                continue;
746            };
747            let window = read.window;
748            let complete = self.fill_clip(
749                &clip,
750                read.first,
751                window.count,
752                window.content,
753                provider,
754                Fill::Presence,
755            );
756            if !complete && !missing.contains(clip.source_id) {
757                missing.push(clip.source_id.clone());
758            }
759        }
760        self.span_scratch = scratch;
761        Ok(())
762    }
763
764    /// Mixes output samples `[start, start + frames)` into `output`, carrying processor state
765    /// forward.
766    ///
767    /// Each stage reads ahead by its latency to the output (master input at
768    /// `start + master_latency`, each bus and clip chain adding its own), so stage outputs line
769    /// up. A replay before a seek to `replay_target` runs only stages whose settling has begun.
770    #[allow(clippy::too_many_arguments)]
771    fn mix_block(
772        &mut self,
773        snapshot: &EngineProject,
774        start: i64,
775        frames: usize,
776        provider: &dyn AudioBlockProvider,
777        transport: TransportState,
778        replay_target: Option<i64>,
779        missing_sources: &mut Vec<SourceId>,
780    ) -> Result<(), EngineError> {
781        let channels = self.layout.channels();
782        let frames_i64 = i64::try_from(frames).map_err(|_| EngineError::OutOfRange)?;
783        let stages = Stages {
784            target: replay_target,
785            block_end: start + frames_i64,
786            lookahead: self.lookahead,
787        };
788        let span = self.block_span(start, frames_i64)?;
789        // Take the scratch out of `self` so the clip loop can still borrow `self` mutably.
790        let mut scratch = std::mem::take(&mut self.span_scratch);
791        let described = match snapshot.evaluate_audio_in(span, &mut scratch) {
792            Ok(described) => described,
793            Err(error) => {
794                self.span_scratch = scratch;
795                return Err(EngineError::Evaluate(error));
796            }
797        };
798
799        for channel in 0..channels {
800            self.mix[channel][..frames].fill(0.0);
801        }
802        for bus in &mut self.buses {
803            bus.live = false;
804        }
805
806        let master_input = start + self.master_latency;
807        self.keep_mixed_chains(&described, master_input, frames, stages);
808        let mut complete_block = true;
809        for clip in described.iter() {
810            let clip = &clip;
811            let Some(read) = self.clip_read(clip, master_input, frames, stages) else {
812                continue;
813            };
814            let window = read.window;
815            let complete = self.fill_clip(
816                clip,
817                read.first,
818                window.count,
819                window.content,
820                provider,
821                Fill::Samples,
822            );
823            complete_block &= complete;
824            if !complete && !missing_sources.contains(clip.source_id) {
825                missing_sources.push(clip.source_id.clone());
826            }
827            if let Some(slot) = read.slot {
828                self.apply_clip_chain(slot, read.first, transport, window.count);
829            }
830            self.mix_clip_into_bus(
831                clip,
832                read.bus,
833                read.bus_input,
834                window.offset,
835                window.count,
836                frames,
837            );
838        }
839        self.span_scratch = scratch;
840        // Drop stretch readers this block did not use.
841        let block = self.blocks_mixed;
842        self.stretch_slots.retain(|_, slot| slot.used_in == block);
843
844        self.process_buses(master_input, transport, frames, stages);
845        self.apply_ducking(&self.ctx(master_input, transport), frames, stages);
846        if stages.run(self.master_lead) {
847            self.sum_buses(master_input, frames);
848            self.finish_master(
849                start,
850                master_input,
851                transport,
852                frames,
853                stages,
854                complete_block,
855            );
856        }
857        self.blocks_mixed += 1;
858        Ok(())
859    }
860
861    /// Drops the chain of every clip the block does not mix, before the block builds any, so only
862    /// one block's clips hold chains at once. A dropped clip mixed again would start from fresh
863    /// state anyway.
864    fn keep_mixed_chains(
865        &mut self,
866        described: &alpha2_eval::AudioSpan<'_, '_>,
867        master_input: i64,
868        frames: usize,
869        stages: Stages,
870    ) {
871        let block = self.blocks_mixed;
872        for clip in described.iter() {
873            let Some((bus, position)) = self
874                .clip_read(&clip, master_input, frames, stages)
875                .and_then(|read| read.slot)
876            else {
877                continue;
878            };
879            if let Some(chain) = self.buses[bus].clips[position].chain.as_mut() {
880                chain.ran_in = block;
881            }
882        }
883        let buses = &mut self.buses;
884        self.live_chains.retain(|&(bus, position)| {
885            let Some(clip) = buses[bus].clips[position].chain.as_mut() else {
886                return false;
887            };
888            if clip.ran_in == block {
889                return true;
890            }
891            clip.chain = None;
892            clip.next_input = None;
893            false
894        });
895    }
896
897    fn ctx(&self, sample: i64, transport: TransportState) -> ProcessCtx {
898        ProcessCtx {
899            position: MediaTime::from_ticks(ticks_at(sample, self.sample_rate)),
900            sample_position: sample,
901            sample_rate: self.sample_rate,
902            transport,
903        }
904    }
905
906    /// Sets the loudness normalization gain in dB, applied between the master gain and the
907    /// limiter. Zero removes it, as does [`Self::set_loudness_metering`].
908    ///
909    /// # Errors
910    /// Returns [`EngineError::InvalidNormalizationGain`] for a non-finite gain or one whose linear
911    /// factor is not a normal `f32` (beyond about 760 dB either way).
912    pub fn set_normalization_gain_db(&mut self, gain_db: f64) -> Result<(), EngineError> {
913        #[allow(clippy::cast_possible_truncation)]
914        let gain = libm::pow(10.0, gain_db / 20.0) as f32;
915        if !gain_db.is_finite() || !gain.is_normal() {
916            return Err(EngineError::InvalidNormalizationGain);
917        }
918        self.normalization_gain = gain;
919        Ok(())
920    }
921
922    /// Whether a block missing a source is held: output as silence with nothing advanced, so
923    /// rendering it again after the host supplies the source matches an uninterrupted render.
924    /// Off by default (live playback); exports turn it on.
925    pub const fn set_hold_incomplete_blocks(&mut self, hold: bool) {
926        self.hold_incomplete = hold;
927    }
928
929    /// Starts (or restarts from empty) measuring the loudness of what reaches the limiter; `false`
930    /// stops. Only complete requested blocks are measured, never replayed history.
931    ///
932    /// Starting removes the normalization gain ([`Self::set_normalization_gain_db`]) so the
933    /// authored mix is measured; a gain set afterwards is included.
934    ///
935    /// # Errors
936    /// Returns [`EngineError::InvalidAudioConfiguration`] for an unsupported layout or rate.
937    pub fn set_loudness_metering(&mut self, enabled: bool) -> Result<(), EngineError> {
938        if enabled {
939            self.normalization_gain = 1.0;
940        }
941        self.meter = if enabled {
942            Some(
943                LoudnessMeter::new(self.sample_rate, self.layout.channels())
944                    .map_err(|_| EngineError::InvalidAudioConfiguration)?,
945            )
946        } else {
947            None
948        };
949        Ok(())
950    }
951
952    /// What the meter has measured, if metering is on.
953    #[must_use]
954    pub fn loudness_report(&self) -> Option<LoudnessReport> {
955        self.meter.as_ref().map(LoudnessMeter::report)
956    }
957
958    /// The master limiter's linear threshold, if any. Pass it to
959    /// [`alpha2_audio::LoudnessNormalization::plan`] so the planned gain stops short of the limiter.
960    #[must_use]
961    pub fn limiter_threshold(&self) -> Option<f64> {
962        self.master
963            .limiter
964            .map(|limiter| f64::from(limiter.threshold))
965    }
966
967    /// Starts (or restarts from empty) measuring the loudness of the final output, after the
968    /// limiter and normalization gain; `false` stops. Unlike [`Self::set_loudness_metering`] it
969    /// leaves the gain unchanged.
970    ///
971    /// # Errors
972    /// Returns [`EngineError::InvalidAudioConfiguration`] for an unsupported layout or rate.
973    pub fn set_output_loudness_metering(&mut self, enabled: bool) -> Result<(), EngineError> {
974        self.output_meter = if enabled {
975            Some(
976                LoudnessMeter::new(self.sample_rate, self.layout.channels())
977                    .map_err(|_| EngineError::InvalidAudioConfiguration)?,
978            )
979        } else {
980            None
981        };
982        Ok(())
983    }
984
985    /// What the output meter has measured, if output metering is on.
986    #[must_use]
987    pub fn output_loudness_report(&self) -> Option<LoudnessReport> {
988        self.output_meter.as_ref().map(LoudnessMeter::report)
989    }
990
991    /// Copies the most recent render into an owned block for offline sinks.
992    #[must_use]
993    pub fn block(&self, status: &AudioRenderStatus) -> AudioBlock {
994        AudioBlock {
995            start_sample: status.start_sample,
996            sample_rate: self.sample_rate,
997            speakers: match self.layout {
998                ChannelLayout::Mono => SpeakerLayout::MONO,
999                ChannelLayout::Stereo => SpeakerLayout::STEREO,
1000            },
1001            channels: (0..self.layout.channels())
1002                .map(|channel| self.output[channel][..status.frames].to_vec())
1003                .collect(),
1004        }
1005    }
1006
1007    /// The bus of a clip's track, by track order or, for another snapshot's clip, by track ID.
1008    fn bus_of(&self, clip: &ActiveAudioClip) -> Option<usize> {
1009        match self.buses.get(clip.track_order) {
1010            Some(bus) if bus.id == *clip.track_id => Some(clip.track_order),
1011            _ => self.bus_of_track.get(clip.track_id).copied(),
1012        }
1013    }
1014
1015    /// A clip's slot, `(bus, position)`, found as [`Self::bus_of`] finds its bus.
1016    fn slot_of(&self, clip: &ActiveAudioClip) -> Option<(usize, usize)> {
1017        let slot = (clip.track_order, clip.clip_order);
1018        let matches = self
1019            .buses
1020            .get(slot.0)
1021            .and_then(|bus| bus.clips.get(slot.1))
1022            .is_some_and(|found| found.id == *clip.clip_id);
1023        if matches {
1024            Some(slot)
1025        } else {
1026            self.slot_of_clip.get(clip.clip_id).copied()
1027        }
1028    }
1029
1030    fn clip_chain(&self, (bus, position): (usize, usize)) -> Option<&ClipChain> {
1031        self.buses[bus].clips[position].chain.as_ref()
1032    }
1033
1034    /// Whether the bus is muted or outside the solo.
1035    fn is_silent_bus(&self, index: usize) -> bool {
1036        self.buses[index].audio.is_silenced(self.any_solo)
1037    }
1038
1039    /// The timeline span evaluated for output samples `[start, start + frames)`, including the
1040    /// lookahead.
1041    fn block_span(&self, start: i64, frames: i64) -> Result<TimeRange, EngineError> {
1042        let invalid = EngineError::InvalidAudioConfiguration;
1043        let span_end = start
1044            .checked_add(frames)
1045            .and_then(|end| end.checked_add(self.lookahead))
1046            .ok_or(invalid.clone())?;
1047        let start_ticks = checked_ticks_at(start, self.sample_rate).ok_or(invalid.clone())?;
1048        let end_ticks = checked_ticks_at(span_end, self.sample_rate)
1049            .zip(start_ticks.checked_add(1))
1050            .map(|(end, after_start)| end.max(after_start))
1051            .ok_or(invalid.clone())?;
1052        TimeRange::new(
1053            MediaTime::from_ticks(start_ticks),
1054            MediaTime::from_ticks(end_ticks),
1055        )
1056        .map_err(|_| invalid)
1057    }
1058
1059    /// Where a block reads a clip, or `None` if it skips it. The mix and [`Self::audio_needs`]
1060    /// share this so a host is asked for exactly what the mix reads.
1061    fn clip_read(
1062        &self,
1063        clip: &ActiveAudioClip,
1064        master_input: i64,
1065        frames: usize,
1066        stages: Stages,
1067    ) -> Option<ClipRead> {
1068        let bus = self.bus_of(clip)?;
1069        if !clip.audio.is_audible() || !self.buses[bus].read {
1070            return None;
1071        }
1072        let bus_input = master_input + self.buses[bus].latency;
1073        let slot = self.slot_of(clip);
1074        let chain = slot.and_then(|slot| self.clip_chain(slot));
1075        let lead = chain.map_or(self.buses[bus].chain_lead, |chain| chain.lead);
1076        if !stages.run(lead) {
1077            return None;
1078        }
1079        let clip_latency = chain.map_or(0, |chain| chain.latency);
1080        let chain_input = bus_input + clip_latency;
1081        let window = self.clip_window(clip, chain_input, frames, clip_latency)?;
1082        Some(ClipRead {
1083            bus,
1084            bus_input,
1085            slot,
1086            first: chain_input + i64::try_from(window.offset).ok()?,
1087            window,
1088        })
1089    }
1090
1091    /// The part of a block of chain input that a clip covers, plus `latency` samples of silence to
1092    /// flush its chain's delay line.
1093    fn clip_window(
1094        &self,
1095        clip: &ActiveAudioClip,
1096        input: i64,
1097        frames: usize,
1098        latency: i64,
1099    ) -> Option<ClipWindow> {
1100        let first = sample_at_or_after(clip.timeline_range.start().ticks(), self.sample_rate);
1101        let last = sample_at_or_after(clip.timeline_range.end().ticks(), self.sample_rate);
1102        let block_end = input.checked_add(i64::try_from(frames).ok()?)?;
1103        let begin = first.max(input);
1104        let end = last.saturating_add(latency).min(block_end);
1105        if end <= begin {
1106            return None;
1107        }
1108        Some(ClipWindow {
1109            offset: usize::try_from(begin - input).ok()?,
1110            count: usize::try_from(end - begin).ok()?,
1111            content: usize::try_from((last.min(end) - begin).max(0)).ok()?,
1112        })
1113    }
1114
1115    /// Reads one clip's source window into `clip_a` (or only checks it, with [`Fill::Presence`]).
1116    /// Returns whether every requested sample came from supplied media.
1117    fn fill_clip(
1118        &mut self,
1119        clip: &ActiveAudioClip,
1120        first_sample: i64,
1121        count: usize,
1122        content: usize,
1123        provider: &dyn AudioBlockProvider,
1124        fill: Fill,
1125    ) -> bool {
1126        if fill == Fill::Samples {
1127            for channel in 0..self.layout.channels() {
1128                self.clip_a[channel][..count].fill(0.0);
1129            }
1130        }
1131        // Past the clip's end is the silence that flushes its chain.
1132        let count = content;
1133        if count == 0 {
1134            return true;
1135        }
1136        let rate = self.sample_rate;
1137        match clip.time_map {
1138            // A frozen clip is silent.
1139            TimeMap::Freeze { .. } => true,
1140            TimeMap::Forward | TimeMap::Reverse if stretches(clip) => {
1141                self.read_stretched(clip, first_sample, count, provider, fill)
1142            }
1143            TimeMap::Forward | TimeMap::Reverse => {
1144                let reversed = clip.time_map == TimeMap::Reverse;
1145                SourceMap::stretched(clip, reversed, rate).is_some_and(|map| {
1146                    self.read_source(clip.source_id, &map, first_sample, 0, count, provider, fill)
1147                })
1148            }
1149            TimeMap::Loop { mode } => {
1150                self.fill_loop(clip, mode, first_sample, count, provider, fill)
1151            }
1152        }
1153    }
1154
1155    /// Reads a looping clip one pass at a time. A repeating loop fades out and in around each wrap.
1156    fn fill_loop(
1157        &mut self,
1158        clip: &ActiveAudioClip,
1159        mode: LoopMode,
1160        first_sample: i64,
1161        count: usize,
1162        provider: &dyn AudioBlockProvider,
1163        fill: Fill,
1164    ) -> bool {
1165        let rate = self.sample_rate;
1166        let end = clip.timeline_range.end().ticks();
1167        #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
1168        let fade = ((f64::from(rate) * LOOP_FADE_SECONDS).round() as i64).max(1);
1169        let mut complete = true;
1170        let mapped = loop_pieces(clip, mode, first_sample, count, rate, |piece| {
1171            complete &= self.read_source(
1172                clip.source_id,
1173                &piece.map,
1174                piece.sample,
1175                piece.index,
1176                piece.length,
1177                provider,
1178                fill,
1179            );
1180            if mode == LoopMode::Repeat && fill == Fill::Samples {
1181                // Fade around wraps inside the clip only; its own ends have authored fades.
1182                let fade_in = (piece.pass > 0).then(|| sample_at_or_after(piece.pass_start, rate));
1183                let fade_out = (piece.pass_end < end).then_some(piece.piece_end);
1184                self.fade_wraps(
1185                    piece.sample,
1186                    piece.index,
1187                    piece.length,
1188                    fade_in,
1189                    fade_out,
1190                    fade,
1191                );
1192            }
1193        });
1194        mapped && complete
1195    }
1196
1197    /// Applies a linear `fade`-sample ramp after the wrap at `fade_in` and before the one at
1198    /// `fade_out`, centred between the samples either side of the wrap.
1199    fn fade_wraps(
1200        &mut self,
1201        first_sample: i64,
1202        offset: usize,
1203        count: usize,
1204        fade_in: Option<i64>,
1205        fade_out: Option<i64>,
1206        fade: i64,
1207    ) {
1208        #[allow(clippy::cast_precision_loss)]
1209        let fade = fade as f64;
1210        for index in 0..count {
1211            let sample = first_sample + i64::try_from(index).unwrap_or(i64::MAX);
1212            #[allow(clippy::cast_precision_loss)]
1213            let after = fade_in.map_or(f64::INFINITY, |wrap| (sample - wrap) as f64 + 0.5);
1214            #[allow(clippy::cast_precision_loss)]
1215            let before = fade_out.map_or(f64::INFINITY, |wrap| (wrap - sample) as f64 - 0.5);
1216            let distance = after.min(before);
1217            if distance >= fade {
1218                continue;
1219            }
1220            #[allow(clippy::cast_possible_truncation)]
1221            let gain = (distance / fade).clamp(0.0, 1.0) as f32;
1222            for channel in 0..self.layout.channels() {
1223                self.clip_a[channel][offset + index] *= gain;
1224            }
1225        }
1226    }
1227
1228    /// Reads `count` project samples from `first_sample` through `map` into `clip_a` at `offset`.
1229    /// Returns whether every requested sample came from supplied media.
1230    #[allow(clippy::too_many_arguments)]
1231    fn read_source(
1232        &mut self,
1233        source_id: &SourceId,
1234        map: &SourceMap,
1235        first_sample: i64,
1236        offset: usize,
1237        count: usize,
1238        provider: &dyn AudioBlockProvider,
1239        fill: Fill,
1240    ) -> bool {
1241        // Positions are exact rationals, so a source aligned with the project grid copies unchanged.
1242        let Some((request_start, request_end)) = map.request(first_sample, count) else {
1243            return false;
1244        };
1245        let request_frames =
1246            usize::try_from(request_end.saturating_sub(request_start)).unwrap_or(0);
1247        let Some(span) = provider.block(source_id, request_start, request_frames) else {
1248            return false;
1249        };
1250        // An uncovered part of the window is media that has not arrived, not silence.
1251        if !span.covers(request_start, request_end) {
1252            return false;
1253        }
1254        let source = span.block;
1255        if source.sample_rate != self.sample_rate {
1256            return false;
1257        }
1258
1259        // The engine downmixes so the fold is the same on every platform.
1260        if source.speakers.channels() != source.channels.len() {
1261            return false;
1262        }
1263        let Some(downmix) = Downmix::new(source.speakers.speakers(), self.layout) else {
1264            return false;
1265        };
1266        if fill == Fill::Presence {
1267            return true;
1268        }
1269        #[cfg(test)]
1270        {
1271            self.samples_read += count;
1272        }
1273        let read = SourceRead {
1274            planes: source.channels.as_slice(),
1275            downmix: &downmix,
1276            start: source.start_sample,
1277            offset,
1278            count,
1279        };
1280        match (map.cursor(first_sample), map.interpolator()) {
1281            // The divisor does not fit the cursor, so divide per sample.
1282            (None, interpolator) => self.read_divided(&read, map, first_sample, interpolator),
1283            (Some(cursor), None) => self.read_copied(&read, cursor),
1284            (Some(cursor), Some(interpolator)) => {
1285                self.read_filtered(&read, map, cursor, interpolator);
1286            }
1287        }
1288        true
1289    }
1290
1291    /// [`Self::read_source`] where every position lands on a whole source sample.
1292    fn read_copied(&mut self, read: &SourceRead<'_>, cursor: RationalCursor) {
1293        let Some(first) = cursor.whole.checked_sub(read.start) else {
1294            return;
1295        };
1296        for channel in 0..self.layout.channels() {
1297            let mix = read.downmix.channel(channel);
1298            let mut at = first;
1299            let outputs = &mut self.clip_a[channel][read.offset..read.offset + read.count];
1300            if let Some(plane) = mix.unity() {
1301                let plane = read.planes[plane].as_slice();
1302                for output in outputs {
1303                    *output = copied(plane, at);
1304                    at = at.wrapping_add(cursor.step_whole);
1305                }
1306            } else {
1307                for output in outputs {
1308                    *output = mixed_sample(mix, read.planes, at);
1309                    at = at.wrapping_add(cursor.step_whole);
1310                }
1311            }
1312        }
1313    }
1314
1315    /// [`Self::read_source`] through `interpolator`. When the fractional positions repeat with a
1316    /// short period (small speed ratios), each phase's weights are computed once per read.
1317    fn read_filtered(
1318        &mut self,
1319        read: &SourceRead<'_>,
1320        map: &SourceMap,
1321        mut cursor: RationalCursor,
1322        interpolator: Interpolator,
1323    ) {
1324        let mut weights = std::mem::take(&mut self.weights);
1325        let reach = interpolator.half_taps() - 1;
1326        let taps = usize::try_from(2 * interpolator.half_taps()).unwrap_or(0);
1327        let phases = usize::try_from(cursor.period())
1328            .ok()
1329            .filter(|&period| period <= read.count / 2 && period * taps <= MAX_CACHED_WEIGHTS);
1330        if let Some(phases) = phases {
1331            weights.resize(phases * taps, 0.0);
1332            let mut scratch = std::mem::take(&mut self.phase_weights);
1333            let mut walk = cursor;
1334            for row in weights.chunks_exact_mut(taps) {
1335                interpolator.weights(map.fraction(&walk).1, &mut scratch);
1336                row.copy_from_slice(&scratch);
1337                walk.advance();
1338            }
1339            self.phase_weights = scratch;
1340        }
1341        let mut phase = 0;
1342        for index in 0..read.count {
1343            let (whole, fraction) = map.fraction(&cursor);
1344            cursor.advance();
1345            let row = if let Some(phases) = phases {
1346                let row = phase;
1347                phase = if phase + 1 == phases { 0 } else { phase + 1 };
1348                &weights[row * taps..(row + 1) * taps]
1349            } else {
1350                interpolator.weights(fraction, &mut weights);
1351                &weights[..]
1352            };
1353            let Some(first) = whole
1354                .checked_sub(read.start)
1355                .and_then(|at| at.checked_sub(reach))
1356            else {
1357                continue;
1358            };
1359            for channel in 0..self.layout.channels() {
1360                self.clip_a[channel][read.offset + index] =
1361                    filtered_sample(read.downmix.channel(channel), read.planes, first, row);
1362            }
1363        }
1364        self.weights = weights;
1365    }
1366
1367    /// [`Self::read_source`] for a map whose divisor does not fit a cursor.
1368    fn read_divided(
1369        &mut self,
1370        read: &SourceRead<'_>,
1371        map: &SourceMap,
1372        first_sample: i64,
1373        interpolator: Option<Interpolator>,
1374    ) {
1375        let mut weights = std::mem::take(&mut self.weights);
1376        for index in 0..read.count {
1377            let sample = first_sample + i64::try_from(index).unwrap_or(0);
1378            let Some((whole, fraction)) = map.position(sample) else {
1379                continue;
1380            };
1381            let Some(at) = whole.checked_sub(read.start) else {
1382                continue;
1383            };
1384            if let Some(interpolator) = interpolator {
1385                interpolator.weights(fraction, &mut weights);
1386            }
1387            for channel in 0..self.layout.channels() {
1388                let mix = read.downmix.channel(channel);
1389                self.clip_a[channel][read.offset + index] = match interpolator {
1390                    None => mixed_sample(mix, read.planes, at),
1391                    Some(interpolator) => filtered_sample(
1392                        mix,
1393                        read.planes,
1394                        at - interpolator.half_taps() + 1,
1395                        &weights,
1396                    ),
1397                };
1398            }
1399        }
1400        self.weights = weights;
1401    }
1402
1403    /// Runs a clip's chain over `count` samples starting at `input`, resetting it unless it
1404    /// continues from where it stopped.
1405    fn apply_clip_chain(
1406        &mut self,
1407        (bus, position): (usize, usize),
1408        input: i64,
1409        transport: TransportState,
1410        count: usize,
1411    ) {
1412        let ctx = self.ctx(input, transport);
1413        let (layout, sample_rate, max_block) = (self.layout, self.sample_rate, self.max_block);
1414        let Some(clip_chain) = self.buses[bus].clips[position].chain.as_mut() else {
1415            return;
1416        };
1417        if clip_chain.chain.is_none() {
1418            self.live_chains.push((bus, position));
1419        }
1420        let effects = &clip_chain.effects;
1421        let chain = clip_chain
1422            .chain
1423            .get_or_insert_with(|| clip_processors(effects, layout, sample_rate, max_block));
1424        clip_chain.ran_in = self.blocks_mixed;
1425        if clip_chain.next_input != Some(input) {
1426            chain.reset();
1427        }
1428        clip_chain.next_input = Some(input + i64::try_from(count).unwrap_or(0));
1429        chain.process_in_place(&ctx, &mut self.clip_a, &mut self.clip_b, count);
1430        #[cfg(test)]
1431        self.note_held();
1432    }
1433
1434    /// Records the clip effects built now in [`Self::most_held`].
1435    #[cfg(test)]
1436    fn note_held(&mut self) {
1437        let held = self
1438            .live_chains
1439            .iter()
1440            .filter_map(|&(bus, position)| self.buses[bus].clips[position].chain.as_ref())
1441            .filter(|clip| clip.chain.is_some())
1442            .fold((0, 0), |(effects, delay), clip| {
1443                (
1444                    effects + clip.effects.len(),
1445                    delay
1446                        + clip
1447                            .effects
1448                            .iter()
1449                            .map(AudioEffect::delay_ticks)
1450                            .sum::<u64>(),
1451                )
1452            });
1453        self.most_held = (self.most_held.0.max(held.0), self.most_held.1.max(held.1));
1454    }
1455
1456    /// Applies gain, fades, and panning, then sums into the bus. Constant gain and pan with no
1457    /// fade in the window are evaluated once; otherwise they follow [`fill_envelope`].
1458    fn mix_clip_into_bus(
1459        &mut self,
1460        clip: &ActiveAudioClip,
1461        bus_index: usize,
1462        start_sample: i64,
1463        offset: usize,
1464        count: usize,
1465        frames: usize,
1466    ) {
1467        let channels = self.layout.channels();
1468        let sample_rate = self.sample_rate;
1469        let bus = &mut self.buses[bus_index];
1470        let first = start_sample + i64::try_from(offset).unwrap_or(0);
1471        bus.make_live(channels, frames);
1472        let gain = constant_value(&clip.audio.gain).map(|gain| gain.clamp(0.0, MAX_GAIN));
1473        let weights =
1474            constant_value(&clip.audio.pan).map(|pan| pan_gains(pan.clamp(-1.0, 1.0), channels));
1475        if let (Some(gain), Some(weights), false) =
1476            (gain, weights, fades_reach(clip, first, count, sample_rate))
1477        {
1478            for (channel, weight) in weights.iter().enumerate().take(channels) {
1479                let input = &self.clip_a[channel][..count];
1480                let output = &mut bus.output[channel][offset..offset + count];
1481                for (output, sample) in output.iter_mut().zip(input) {
1482                    *output += sample * gain * weight;
1483                }
1484            }
1485            return;
1486        }
1487        let audio = clip.audio;
1488        fill_envelope(
1489            &mut self.envelope[..],
1490            first,
1491            count,
1492            sample_rate,
1493            |ticks| {
1494                earliest(
1495                    earliest(
1496                        next_keyframe(&audio.gain, ticks),
1497                        next_keyframe(&audio.pan, ticks),
1498                    ),
1499                    next_fade_edge(clip, ticks),
1500                )
1501            },
1502            |time| {
1503                let gain =
1504                    sample_scalar(&audio.gain, time).clamp(0.0, MAX_GAIN) * fade_gain(clip, time);
1505                let [left, right] = pan_gains(sample_scalar(&audio.pan, time), channels);
1506                [gain, left, right]
1507            },
1508        );
1509        let [gains, left, right] = &self.envelope;
1510        for (channel, weights) in [left, right].into_iter().enumerate().take(channels) {
1511            let input = &self.clip_a[channel][..count];
1512            let output = &mut bus.output[channel][offset..offset + count];
1513            for (((output, sample), gain), weight) in
1514                output.iter_mut().zip(input).zip(gains).zip(weights)
1515            {
1516                *output += sample * gain * weight;
1517            }
1518        }
1519    }
1520
1521    fn process_buses(
1522        &mut self,
1523        master_input: i64,
1524        transport: TransportState,
1525        frames: usize,
1526        stages: Stages,
1527    ) {
1528        let channels = self.layout.channels();
1529        let sample_rate = self.sample_rate;
1530        for bus in &mut self.buses {
1531            if !stages.run(bus.chain_lead) || bus.chain.passes_through() {
1532                continue;
1533            }
1534            let input = master_input + bus.latency;
1535            let ctx = &ProcessCtx {
1536                position: MediaTime::from_ticks(ticks_at(input, sample_rate)),
1537                sample_position: input,
1538                sample_rate,
1539                transport,
1540            };
1541            bus.make_live(channels, frames);
1542            for channel in 0..channels {
1543                self.bus_in[channel][..frames].copy_from_slice(&bus.output[channel][..frames]);
1544            }
1545            match channels {
1546                1 => bus.chain.process(
1547                    ctx,
1548                    &[&self.bus_in[0][..frames]],
1549                    &mut [&mut bus.output[0][..frames]],
1550                ),
1551                2 => {
1552                    let (left, right) = bus.output.split_at_mut(1);
1553                    bus.chain.process(
1554                        ctx,
1555                        &[&self.bus_in[0][..frames], &self.bus_in[1][..frames]],
1556                        &mut [&mut left[0][..frames], &mut right[0][..frames]],
1557                    );
1558                }
1559                _ => {}
1560            }
1561        }
1562    }
1563
1564    /// Attenuates ducked buses from the pre-duck output of their sidechain.
1565    ///
1566    /// Every ducker writes its own buffer before any bus `output` changes, so a ducked sidechain
1567    /// is read pre-duck regardless of track order.
1568    fn apply_ducking(&mut self, ctx: &ProcessCtx, frames: usize, stages: Stages) {
1569        let channels = self.layout.channels();
1570        let ducks = |bus: &TrackBus, index: usize| {
1571            bus.active_sidechain(index).is_some() && stages.run(bus.ducker_lead)
1572        };
1573        for index in 0..self.buses.len() {
1574            if !ducks(&self.buses[index], index) {
1575                continue;
1576            }
1577            let Some(sidechain) = self.buses[index].active_sidechain(index) else {
1578                continue;
1579            };
1580            // Either bus may be idle; the ducker still runs on silence.
1581            self.buses[sidechain].make_live(channels, frames);
1582            for channel in 0..channels {
1583                self.bus_in[channel][..frames]
1584                    .copy_from_slice(&self.buses[sidechain].output[channel][..frames]);
1585            }
1586            let bus = &mut self.buses[index];
1587            bus.make_live(channels, frames);
1588            let Some(BusDucker { ducker, output }) = bus.ducker.as_mut() else {
1589                continue;
1590            };
1591            match channels {
1592                1 => ducker.process(
1593                    ctx,
1594                    &[&bus.output[0][..frames], &self.bus_in[0][..frames]],
1595                    &mut [&mut output[0][..frames]],
1596                ),
1597                2 => {
1598                    let (left, right) = output.split_at_mut(1);
1599                    ducker.process(
1600                        ctx,
1601                        &[
1602                            &bus.output[0][..frames],
1603                            &bus.output[1][..frames],
1604                            &self.bus_in[0][..frames],
1605                            &self.bus_in[1][..frames],
1606                        ],
1607                        &mut [&mut left[0][..frames], &mut right[0][..frames]],
1608                    );
1609                }
1610                _ => {}
1611            }
1612        }
1613        for (index, bus) in self.buses.iter_mut().enumerate() {
1614            if ducks(bus, index)
1615                && let Some(ducking) = bus.ducker.as_mut()
1616            {
1617                std::mem::swap(&mut bus.output, &mut ducking.output);
1618            }
1619        }
1620    }
1621
1622    fn sum_buses(&mut self, start_sample: i64, frames: usize) {
1623        let channels = self.layout.channels();
1624        for bus_index in 0..self.buses.len() {
1625            // Skipping an idle bus is exact: adding `+0.0` to a sum that is never `-0.0` is a no-op.
1626            if self.is_silent_bus(bus_index) || !self.buses[bus_index].live {
1627                continue;
1628            }
1629            let bus = &self.buses[bus_index];
1630            #[cfg(test)]
1631            BUS_PLANES_SUMMED.set(BUS_PLANES_SUMMED.get() + channels);
1632            let gain = constant_value(&bus.audio.gain).map(|gain| gain.clamp(0.0, MAX_GAIN));
1633            let weights =
1634                constant_value(&bus.audio.pan).map(|pan| pan_gains(pan.clamp(-1.0, 1.0), channels));
1635            if let (Some(gain), Some(weights)) = (gain, weights) {
1636                for (channel, weight) in weights.iter().enumerate().take(channels) {
1637                    let input = &bus.output[channel][..frames];
1638                    let output = &mut self.mix[channel][..frames];
1639                    for (output, sample) in output.iter_mut().zip(input) {
1640                        *output += sample * gain * weight;
1641                    }
1642                }
1643                continue;
1644            }
1645            let audio = &bus.audio;
1646            fill_envelope(
1647                &mut self.envelope[..],
1648                start_sample,
1649                frames,
1650                self.sample_rate,
1651                |ticks| {
1652                    earliest(
1653                        next_keyframe(&audio.gain, ticks),
1654                        next_keyframe(&audio.pan, ticks),
1655                    )
1656                },
1657                |time| {
1658                    let gain = sample_scalar(&audio.gain, time).clamp(0.0, MAX_GAIN);
1659                    let [left, right] = pan_gains(sample_scalar(&audio.pan, time), channels);
1660                    [gain, left, right]
1661                },
1662            );
1663            let [gains, left, right] = &self.envelope;
1664            for (channel, weights) in [left, right].into_iter().enumerate().take(channels) {
1665                let input = &bus.output[channel][..frames];
1666                let output = &mut self.mix[channel][..frames];
1667                for (((output, sample), gain), weight) in
1668                    output.iter_mut().zip(input).zip(gains).zip(weights)
1669                {
1670                    *output += sample * gain * weight;
1671                }
1672            }
1673        }
1674    }
1675
1676    fn finish_master(
1677        &mut self,
1678        start_sample: i64,
1679        master_input: i64,
1680        transport: TransportState,
1681        frames: usize,
1682        stages: Stages,
1683        complete: bool,
1684    ) {
1685        let channels = self.layout.channels();
1686        let input_ctx = self.ctx(master_input, transport);
1687        let ctx = &self.ctx(start_sample, transport);
1688        match channels {
1689            1 => self.master_chain.process(
1690                &input_ctx,
1691                &[&self.mix[0][..frames]],
1692                &mut [&mut self.output[0][..frames]],
1693            ),
1694            2 => {
1695                let (left, right) = self.output.split_at_mut(1);
1696                self.master_chain.process(
1697                    &input_ctx,
1698                    &[&self.mix[0][..frames], &self.mix[1][..frames]],
1699                    &mut [&mut left[0][..frames], &mut right[0][..frames]],
1700                );
1701            }
1702            _ => {}
1703        }
1704        if let Some(gain) = constant_value(&self.master.gain) {
1705            let gain = gain.clamp(0.0, MAX_GAIN);
1706            for channel in 0..channels {
1707                for sample in &mut self.output[channel][..frames] {
1708                    *sample *= gain;
1709                }
1710            }
1711        } else {
1712            let gain = &self.master.gain;
1713            fill_envelope(
1714                &mut self.envelope[..],
1715                start_sample,
1716                frames,
1717                self.sample_rate,
1718                |ticks| next_keyframe(gain, ticks),
1719                |time| [sample_scalar(gain, time).clamp(0.0, MAX_GAIN)],
1720            );
1721            for channel in 0..channels {
1722                let envelope = &self.envelope[0][..frames];
1723                for (sample, gain) in self.output[channel][..frames].iter_mut().zip(envelope) {
1724                    *sample *= gain;
1725                }
1726            }
1727        }
1728        if self.normalization_gain.to_bits() != 1.0_f32.to_bits() {
1729            for channel in 0..channels {
1730                for sample in &mut self.output[channel][..frames] {
1731                    *sample *= self.normalization_gain;
1732                }
1733            }
1734        }
1735        // Meter only complete requested blocks, never replayed history or blocks missing a source.
1736        let measured = stages.target.is_none() && complete;
1737        if measured && let Some(meter) = self.meter.as_mut() {
1738            let planes = [&self.output[0][..frames], &self.output[1][..frames]];
1739            // The meter was made for this layout, so the block always fits.
1740            let _ = meter.push(&planes[..channels]);
1741        }
1742        if let Some(limiter) = self
1743            .limiter
1744            .as_mut()
1745            .filter(|_| stages.run(self.limiter_lead))
1746        {
1747            for channel in 0..channels {
1748                self.mix[channel][..frames].copy_from_slice(&self.output[channel][..frames]);
1749            }
1750            match channels {
1751                1 => limiter.process(
1752                    ctx,
1753                    &[&self.mix[0][..frames]],
1754                    &mut [&mut self.output[0][..frames]],
1755                ),
1756                2 => {
1757                    let (left, right) = self.output.split_at_mut(1);
1758                    limiter.process(
1759                        ctx,
1760                        &[&self.mix[0][..frames], &self.mix[1][..frames]],
1761                        &mut [&mut left[0][..frames], &mut right[0][..frames]],
1762                    );
1763                }
1764                _ => {}
1765            }
1766        }
1767        if measured && let Some(meter) = self.output_meter.as_mut() {
1768            let planes = [&self.output[0][..frames], &self.output[1][..frames]];
1769            let _ = meter.push(&planes[..channels]);
1770        }
1771    }
1772}
1773
1774fn track_buses(
1775    project: &alpha2_model::Project,
1776    layout: ChannelLayout,
1777    sample_rate: u32,
1778    max_block: usize,
1779) -> (Vec<TrackBus>, HashMap<TrackId, usize>) {
1780    let mut buses = Vec::new();
1781    let mut bus_of_track = HashMap::new();
1782    // Every track gets a bus, since a clip on a visual track can carry audio.
1783    for track in &project.timeline.tracks {
1784        let mut chain = ProcessorChain::new(layout);
1785        push_effects(&mut chain, &track.audio.effects);
1786        chain.prepare(sample_rate, max_block);
1787        let chain_latency = chain.latency_samples();
1788        let ducker = track.audio.ducking.as_ref().map(|ducking| {
1789            let mut ducker = Ducker::new(
1790                ducking.threshold,
1791                ducking.depth_db,
1792                ducking.attack_ms,
1793                ducking.release_ms,
1794            );
1795            ducker.prepare(sample_rate, max_block);
1796            BusDucker {
1797                ducker,
1798                output: buffers(max_block),
1799            }
1800        });
1801        bus_of_track.insert(track.id.clone(), buses.len());
1802        buses.push(TrackBus {
1803            id: track.id.clone(),
1804            audio: track.audio.clone(),
1805            chain,
1806            ducker,
1807            sidechain: None,
1808            output: buffers(max_block),
1809            live: false,
1810            latency: i64::from(chain_latency),
1811            clips: clip_slots(&track.clips, layout, sample_rate, max_block),
1812            chain_lead: 0,
1813            ducker_lead: 0,
1814            read: false,
1815        });
1816    }
1817    (buses, bus_of_track)
1818}
1819
1820/// One slot per clip. Chains are measured here and built when their clip is mixed.
1821fn clip_slots(
1822    clips: &[alpha2_model::Clip],
1823    layout: ChannelLayout,
1824    sample_rate: u32,
1825    max_block: usize,
1826) -> Vec<ClipSlot> {
1827    clips
1828        .iter()
1829        .map(|clip| ClipSlot {
1830            id: clip.id.clone(),
1831            chain: (!clip.audio.effects.is_empty()).then(|| {
1832                // Built once to measure latency and history, then dropped.
1833                let chain = clip_processors(&clip.audio.effects, layout, sample_rate, max_block);
1834                ClipChain {
1835                    effects: clip.audio.effects.clone(),
1836                    chain: None,
1837                    ran_in: 0,
1838                    latency: i64::from(chain.latency_samples()),
1839                    history: chain.history_samples(),
1840                    next_input: None,
1841                    lead: 0,
1842                }
1843            }),
1844        })
1845        .collect()
1846}
1847
1848fn clip_processors(
1849    effects: &[AudioEffect],
1850    layout: ChannelLayout,
1851    sample_rate: u32,
1852    max_block: usize,
1853) -> ProcessorChain {
1854    let mut chain = ProcessorChain::new(layout);
1855    push_effects(&mut chain, effects);
1856    chain.prepare_nodes(sample_rate, max_block);
1857    chain
1858}
1859
1860/// The longest latency from any clip's input to its bus's output.
1861fn longest_path_latency(buses: &[TrackBus]) -> i64 {
1862    buses
1863        .iter()
1864        .map(|bus| {
1865            bus.latency
1866                + bus
1867                    .clips
1868                    .iter()
1869                    .filter_map(|slot| slot.chain.as_ref())
1870                    .map(|chain| chain.latency)
1871                    .max()
1872                    .unwrap_or(0)
1873        })
1874        .max()
1875        .unwrap_or(0)
1876}
1877
1878/// Gives every bus, ducker, and clip chain its lead, and returns the longest.
1879///
1880/// A stage's lead is its own history plus the lead of the stage consuming its output. A bus that
1881/// feeds both the master sum and other buses' duckers takes the longest of their leads.
1882fn assign_leads(buses: &mut [TrackBus], master_lead: i64, settling: Settling) -> i64 {
1883    let ducker_leads: Vec<Option<i64>> = buses
1884        .iter()
1885        .enumerate()
1886        .map(|(index, bus)| {
1887            let ducking = bus.ducker.as_ref()?;
1888            bus.active_sidechain(index)?;
1889            Some(settling.lead(master_lead, ducking.ducker.history_samples()))
1890        })
1891        .collect();
1892    let mut longest = master_lead;
1893    for index in 0..buses.len() {
1894        let sidechained = buses
1895            .iter()
1896            .enumerate()
1897            .filter(|&(user, bus)| bus.active_sidechain(user) == Some(index))
1898            .filter_map(|(user, _)| ducker_leads[user]);
1899        let settled_from = sidechained
1900            .chain(ducker_leads[index])
1901            .fold(master_lead, i64::max);
1902        let bus = &mut buses[index];
1903        bus.ducker_lead = ducker_leads[index].unwrap_or(0);
1904        bus.chain_lead = settling.lead(settled_from, bus.chain.history_samples());
1905        longest = longest.max(bus.chain_lead);
1906        for clip in bus.clips.iter_mut().filter_map(|slot| slot.chain.as_mut()) {
1907            clip.lead = settling.lead(bus.chain_lead, clip.history);
1908            longest = longest.max(clip.lead);
1909        }
1910    }
1911    longest
1912}
1913
1914/// The lead cap in samples ([`MAX_SETTLING_SECONDS`]).
1915#[derive(Clone, Copy)]
1916struct Settling(i64);
1917
1918impl Settling {
1919    /// The lead of a stage with `history` feeding one with lead `downstream`. Never shorter than
1920    /// `downstream`, otherwise capped.
1921    fn lead(self, downstream: i64, history: u32) -> i64 {
1922        downstream
1923            .saturating_add(i64::from(history))
1924            .min(self.0.max(downstream))
1925    }
1926}
1927
1928/// Which stages a block runs. In a replay before a seek to `target`, a stage runs once its lead
1929/// has begun; otherwise all run.
1930#[derive(Clone, Copy)]
1931struct Stages {
1932    target: Option<i64>,
1933    block_end: i64,
1934    lookahead: i64,
1935}
1936
1937impl Stages {
1938    fn run(self, lead: i64) -> bool {
1939        self.target
1940            .is_none_or(|target| self.block_end > Self::start(target, lead, self.lookahead))
1941    }
1942
1943    /// The output sample from which a stage with `lead` runs in the replay. As in
1944    /// `preroll_start`, history stops at timeline zero but the lookahead reaches before it.
1945    fn start(target: i64, lead: i64, lookahead: i64) -> i64 {
1946        (target - lead).max(0).saturating_sub(lookahead)
1947    }
1948}
1949
1950/// Every distinct stage lead, longest first.
1951fn stage_leads(buses: &[TrackBus], master_lead: i64, limiter_lead: i64) -> Vec<i64> {
1952    let mut leads = vec![master_lead, limiter_lead];
1953    for (index, bus) in buses.iter().enumerate() {
1954        leads.push(bus.chain_lead);
1955        if bus.active_sidechain(index).is_some() {
1956            leads.push(bus.ducker_lead);
1957        }
1958        leads.extend(
1959            bus.clips
1960                .iter()
1961                .filter_map(|slot| slot.chain.as_ref())
1962                .map(|chain| chain.lead),
1963        );
1964    }
1965    leads.sort_unstable_by(|a, b| b.cmp(a));
1966    leads.dedup();
1967    leads
1968}
1969
1970/// Marks the buses whose clips the mix reads: every heard bus and every unmuted bus keying the
1971/// ducker of a heard one. Solo changes what is heard, not how a ducked track sounds, so a soloed
1972/// track still reads its sidechain.
1973fn read_buses(buses: &mut [TrackBus], any_solo: bool) {
1974    let heard: Vec<bool> = buses
1975        .iter()
1976        .map(|bus| !bus.audio.is_silenced(any_solo))
1977        .collect();
1978    let keys: Vec<Option<usize>> = buses
1979        .iter()
1980        .enumerate()
1981        .map(|(index, bus)| heard[index].then(|| bus.active_sidechain(index)).flatten())
1982        .collect();
1983    for (index, bus) in buses.iter_mut().enumerate() {
1984        bus.read = heard[index] || (!bus.audio.muted && keys.contains(&Some(index)));
1985    }
1986}
1987
1988fn resolve_sidechains(buses: &mut [TrackBus], bus_of_track: &HashMap<TrackId, usize>) {
1989    for bus in buses {
1990        bus.sidechain = bus
1991            .audio
1992            .ducking
1993            .as_ref()
1994            .and_then(|ducking| bus_of_track.get(&ducking.sidechain_track).copied());
1995    }
1996}
1997
1998fn buffers(max_block: usize) -> [Vec<f32>; 2] {
1999    [vec![0.0; max_block], vec![0.0; max_block]]
2000}
2001
2002fn push_effects(chain: &mut ProcessorChain, effects: &[AudioEffect]) {
2003    for effect in effects {
2004        match effect {
2005            AudioEffect::Eq { bands } => {
2006                for band in bands {
2007                    push_effect(
2008                        chain,
2009                        Biquad::new(
2010                            match band.kind {
2011                                EqBandKind::Peaking => BiquadKind::Peaking,
2012                                EqBandKind::LowPass => BiquadKind::LowPass,
2013                                EqBandKind::HighPass => BiquadKind::HighPass,
2014                            },
2015                            band.frequency_hz,
2016                            band.q,
2017                            band.gain_db,
2018                        ),
2019                    );
2020                }
2021            }
2022            AudioEffect::Compressor {
2023                threshold_db,
2024                ratio,
2025                attack_ms,
2026                release_ms,
2027                lookahead_ms,
2028            } => push_effect(
2029                chain,
2030                Compressor::new(
2031                    *threshold_db,
2032                    *ratio,
2033                    *attack_ms,
2034                    *release_ms,
2035                    *lookahead_ms,
2036                ),
2037            ),
2038            AudioEffect::Reverb {
2039                room_size,
2040                damping,
2041                mix,
2042            } => push_effect(chain, SchroederReverb::new(*room_size, *damping, *mix)),
2043        }
2044    }
2045}
2046
2047fn push_effect(chain: &mut ProcessorChain, effect: impl Processor + 'static) {
2048    chain
2049        .push(effect)
2050        .expect("a model effect reads one input per output");
2051}
2052
2053fn ticks_at(sample: i64, sample_rate: u32) -> i64 {
2054    checked_ticks_at(sample, sample_rate).unwrap_or(i64::MAX)
2055}
2056
2057/// The tick position of a sample, or `None` when it does not fit in ticks.
2058fn checked_ticks_at(sample: i64, sample_rate: u32) -> Option<i64> {
2059    let rate = i128::from(sample_rate.max(1));
2060    i64::try_from((i128::from(sample) * i128::from(TICKS_PER_SECOND)).div_euclid(rate)).ok()
2061}
2062
2063/// A source block and where its samples go in `clip_a`.
2064struct SourceRead<'a> {
2065    planes: &'a [Vec<f32>],
2066    downmix: &'a Downmix,
2067    /// The source sample the planes start at.
2068    start: i64,
2069    offset: usize,
2070    count: usize,
2071}
2072
2073/// How one block reads one clip: see [`AudioRenderer::clip_read`].
2074struct ClipRead {
2075    bus: usize,
2076    bus_input: i64,
2077    slot: Option<(usize, usize)>,
2078    /// The project sample of the clip's first read sample.
2079    first: i64,
2080    window: ClipWindow,
2081}
2082
2083/// Where a clip lands in one block of its chain's input.
2084#[derive(Clone, Copy)]
2085struct ClipWindow {
2086    /// First block sample the clip covers.
2087    offset: usize,
2088    /// Samples the chain runs for, including the flush.
2089    count: usize,
2090    /// Of those, samples read from the source; the rest are silence.
2091    content: usize,
2092}
2093
2094/// The first tick at or after a sample, or `None` when it does not fit.
2095fn ticks_at_or_after(sample: i64, sample_rate: u32) -> Option<i64> {
2096    let rate = i128::from(sample_rate.max(1));
2097    let numerator = i128::from(sample) * i128::from(TICKS_PER_SECOND);
2098    i64::try_from(numerator.div_euclid(rate) + i128::from(numerator.rem_euclid(rate) != 0)).ok()
2099}
2100
2101/// Adds `source_range` to `needs`, merged with every need of that source it overlaps or touches.
2102fn merge_need(needs: &mut Vec<AudioNeed>, source_id: &SourceId, mut source_range: TimeRange) {
2103    // A new range can bridge two existing needs, so keep absorbing until none touch.
2104    while let Some(index) = needs.iter().position(|need| {
2105        need.source_id == *source_id
2106            && need.source_range.start() <= source_range.end()
2107            && source_range.start() <= need.source_range.end()
2108    }) {
2109        let existing = needs.remove(index).source_range;
2110        let merged_start = existing.start().min(source_range.start());
2111        let merged_end = existing.end().max(source_range.end());
2112        if let Ok(merged) = TimeRange::new(merged_start, merged_end) {
2113            source_range = merged;
2114        }
2115    }
2116    needs.push(AudioNeed {
2117        source_id: source_id.clone(),
2118        source_range,
2119    });
2120}
2121
2122/// Calls `request` with each source window `[from, to)` that [`AudioRenderer::fill_clip`] would
2123/// read for the same arguments.
2124fn source_requests(
2125    stretch: &Stretch,
2126    clip: &ActiveAudioClip,
2127    first_sample: i64,
2128    count: usize,
2129    rate: u32,
2130    mut request: impl FnMut(i64, i64),
2131) {
2132    if count == 0 {
2133        return;
2134    }
2135    match clip.time_map {
2136        TimeMap::Freeze { .. } => {}
2137        TimeMap::Forward | TimeMap::Reverse if stretches(clip) => {
2138            if let Some((from, to)) = stretched_request(stretch, clip, first_sample, count, rate) {
2139                request(from, to);
2140            }
2141        }
2142        TimeMap::Forward | TimeMap::Reverse => {
2143            let reversed = clip.time_map == TimeMap::Reverse;
2144            if let Some((from, to)) = SourceMap::stretched(clip, reversed, rate)
2145                .and_then(|map| map.request(first_sample, count))
2146            {
2147                request(from, to);
2148            }
2149        }
2150        TimeMap::Loop { mode } => {
2151            loop_pieces(clip, mode, first_sample, count, rate, |piece| {
2152                if let Some((from, to)) = piece.map.request(piece.sample, piece.length) {
2153                    request(from, to);
2154                }
2155            });
2156        }
2157    }
2158}
2159
2160/// One pass's share of a looping clip's read.
2161struct LoopPiece {
2162    /// The project sample the piece starts at; `index` is its offset into the read.
2163    sample: i64,
2164    index: usize,
2165    length: usize,
2166    /// The pass number; `pass_start` and `pass_end` are in timeline ticks.
2167    pass: i64,
2168    pass_start: i64,
2169    pass_end: i64,
2170    /// The first project sample of the next pass.
2171    piece_end: i64,
2172    map: SourceMap,
2173}
2174
2175/// Cuts a looping clip's read at its wraps and calls `piece` for each pass. Returns false if the
2176/// clip cannot be mapped.
2177fn loop_pieces(
2178    clip: &ActiveAudioClip,
2179    mode: LoopMode,
2180    first_sample: i64,
2181    count: usize,
2182    rate: u32,
2183    mut piece: impl FnMut(LoopPiece),
2184) -> bool {
2185    let Some(period) = clip.source_range.duration().map(MediaTime::ticks) else {
2186        return false;
2187    };
2188    if period <= 0 {
2189        return false;
2190    }
2191    let start = clip.timeline_range.start().ticks();
2192    let mut index = 0;
2193    while index < count {
2194        let sample = first_sample + i64::try_from(index).unwrap_or(i64::MAX);
2195        let pass = (ticks_at(sample, rate) - start).div_euclid(period);
2196        let Some(pass_start) = pass
2197            .checked_mul(period)
2198            .and_then(|ticks| ticks.checked_add(start))
2199        else {
2200            return false;
2201        };
2202        let pass_end = pass_start.saturating_add(period);
2203        let piece_end = sample_at_or_after(pass_end, rate);
2204        let length = usize::try_from(piece_end.saturating_sub(sample))
2205            .unwrap_or(usize::MAX)
2206            .clamp(1, count - index);
2207        let reversed = mode == LoopMode::PingPong && pass.rem_euclid(2) == 1;
2208        let Some(map) = SourceMap::pass(clip, pass_start, reversed, rate) else {
2209            return false;
2210        };
2211        piece(LoopPiece {
2212            sample,
2213            index,
2214            length,
2215            pass,
2216            pass_start,
2217            pass_end,
2218            piece_end,
2219            map,
2220        });
2221        index += length;
2222    }
2223    true
2224}
2225
2226fn sample_at_or_after(ticks: i64, sample_rate: u32) -> i64 {
2227    let rate = i128::from(sample_rate.max(1));
2228    let numerator = i128::from(ticks) * rate;
2229    let denominator = i128::from(TICKS_PER_SECOND);
2230    let value =
2231        numerator.div_euclid(denominator) + i128::from(numerator.rem_euclid(denominator) != 0);
2232    i64::try_from(value).unwrap_or(i64::MAX)
2233}
2234
2235/// One project channel's sample at source sample `index`, folded by `mix`. Terms are summed in
2236/// plane order so the result is the same on every platform.
2237fn mixed_sample(mix: &ChannelMix, planes: &[Vec<f32>], index: i64) -> f32 {
2238    if let Some(plane) = mix.unity() {
2239        return copied(&planes[plane], index);
2240    }
2241    let mut sum = 0.0;
2242    for &(plane, weight) in mix.terms() {
2243        sum += weight * copied(&planes[plane], index);
2244    }
2245    sum
2246}
2247
2248/// Like [`mixed_sample`], filtering source samples `first..` by `weights`.
2249fn filtered_sample(mix: &ChannelMix, planes: &[Vec<f32>], first: i64, weights: &[f64]) -> f32 {
2250    if let Some(plane) = mix.unity() {
2251        return filtered(&planes[plane], first, weights);
2252    }
2253    let mut sum = 0.0;
2254    for &(plane, weight) in mix.terms() {
2255        sum += weight * filtered(&planes[plane], first, weights);
2256    }
2257    sum
2258}
2259
2260/// Source sample `index` of `plane`. Samples outside the plane or non-finite are silence; reads
2261/// happen only where the block covers the request, so this matches explicit zeros.
2262fn copied(plane: &[f32], index: i64) -> f32 {
2263    let value = usize::try_from(index)
2264        .ok()
2265        .and_then(|index| plane.get(index).copied())
2266        .unwrap_or(0.0);
2267    if value.is_finite() { value } else { 0.0 }
2268}
2269
2270/// The weighted sum of `plane[first..first + weights.len()]`, treating samples as [`copied`] does.
2271fn filtered(plane: &[f32], first: i64, weights: &[f64]) -> f32 {
2272    let mut sum = weighed(plane, first, weights, false);
2273    if !sum.is_finite() {
2274        sum = weighed(plane, first, weights, true);
2275    }
2276    #[allow(clippy::cast_possible_truncation)]
2277    let value = sum as f32;
2278    if value.is_finite() { value } else { 0.0 }
2279}
2280
2281/// The sum behind [`filtered`]. Four interleaved running sums added pairwise give a fixed order
2282/// (identical on every platform) without one long dependency chain.
2283fn weighed(plane: &[f32], first: i64, weights: &[f64], finite_only: bool) -> f64 {
2284    let inside = usize::try_from(first).ok().filter(|&from| {
2285        from.checked_add(weights.len())
2286            .is_some_and(|end| end <= plane.len())
2287    });
2288    let mut sums = [0.0_f64; 4];
2289    match inside {
2290        Some(from) if !finite_only => {
2291            let (weight_quads, weight_rest) = weights.as_chunks::<4>();
2292            let (sample_quads, sample_rest) = plane[from..from + weights.len()].as_chunks::<4>();
2293            for (weight, value) in weight_quads.iter().zip(sample_quads) {
2294                for lane in 0..4 {
2295                    sums[lane] += weight[lane] * f64::from(value[lane]);
2296                }
2297            }
2298            for (lane, (weight, value)) in weight_rest.iter().zip(sample_rest).enumerate() {
2299                sums[lane] += weight * f64::from(*value);
2300            }
2301        }
2302        _ => {
2303            for (index, (at, weight)) in (first..).zip(weights).enumerate() {
2304                let value = usize::try_from(at)
2305                    .ok()
2306                    .and_then(|at| plane.get(at).copied())
2307                    .unwrap_or(0.0);
2308                if value.is_finite() {
2309                    sums[index % 4] += weight * f64::from(value);
2310                }
2311            }
2312        }
2313    }
2314    (sums[0] + sums[1]) + (sums[2] + sums[3])
2315}
2316
2317/// Where each project sample of a clip reads its source, in source samples.
2318///
2319/// `p(n) = (a + n * b) / d` with integer terms, so positions are exact and independent of the
2320/// tick grid.
2321struct SourceMap {
2322    a: i128,
2323    b: i128,
2324    d: i128,
2325}
2326
2327impl SourceMap {
2328    /// A forward or reverse clip's timeline range stretched over its source range. Reversed reads
2329    /// the forward positions in the opposite order.
2330    fn stretched(clip: &ActiveAudioClip, reversed: bool, rate: u32) -> Option<Self> {
2331        let timeline = clip.timeline_range;
2332        let forward = Self::line(
2333            i128::from(timeline.start().ticks()),
2334            i128::from(clip.source_range.start().ticks()),
2335            i128::from(timeline.duration()?.ticks()),
2336            i128::from(clip.source_range.duration()?.ticks()),
2337            rate,
2338        )?;
2339        if !reversed {
2340            return Some(forward);
2341        }
2342        forward.mirrored(
2343            sample_at_or_after(timeline.start().ticks(), rate),
2344            sample_at_or_after(timeline.end().ticks(), rate),
2345        )
2346    }
2347
2348    /// One loop pass starting at timeline tick `pass_start`, at source speed.
2349    fn pass(clip: &ActiveAudioClip, pass_start: i64, reversed: bool, rate: u32) -> Option<Self> {
2350        let period = clip.source_range.duration()?.ticks();
2351        let forward = Self::line(
2352            i128::from(pass_start),
2353            i128::from(clip.source_range.start().ticks()),
2354            i128::from(period),
2355            i128::from(period),
2356            rate,
2357        )?;
2358        if !reversed {
2359            return Some(forward);
2360        }
2361        forward.mirrored(
2362            sample_at_or_after(pass_start, rate),
2363            sample_at_or_after(pass_start.checked_add(period)?, rate),
2364        )
2365    }
2366
2367    /// This map mirrored over `first..last`: sample `n` reads what `first + last - 1 - n` reads.
2368    fn mirrored(&self, first: i64, last: i64) -> Option<Self> {
2369        let pivot = i128::from(first)
2370            .checked_add(i128::from(last))?
2371            .checked_sub(1)?;
2372        Some(Self {
2373            a: self.a.checked_add(pivot.checked_mul(self.b)?)?,
2374            b: self.b.checked_neg()?,
2375            d: self.d,
2376        })
2377    }
2378
2379    /// The line through source tick `s0` at timeline tick `t0`, advancing
2380    /// `source` ticks per `timeline` ticks.
2381    fn line(t0: i128, s0: i128, timeline: i128, source: i128, rate: u32) -> Option<Self> {
2382        if timeline <= 0 || source <= 0 {
2383            return None;
2384        }
2385        let divisor = gcd(timeline, source);
2386        let (timeline, source) = (timeline / divisor, source / divisor);
2387        let rate = i128::from(rate);
2388        let ticks = i128::from(TICKS_PER_SECOND);
2389        // p(n) = (s0 + (n * TPS / rate - t0) * source / timeline) * rate / TPS
2390        let a = rate.checked_mul(
2391            s0.checked_mul(timeline)?
2392                .checked_sub(t0.checked_mul(source)?)?,
2393        )?;
2394        let b = ticks.checked_mul(source)?;
2395        let d = ticks.checked_mul(timeline)?;
2396        Some(Self { a, b, d })
2397    }
2398
2399    /// The source sample at or before `sample`, and the fraction toward the next.
2400    fn position(&self, sample: i64) -> Option<(i64, f64)> {
2401        let numerator = self
2402            .a
2403            .checked_add(i128::from(sample).checked_mul(self.b)?)?;
2404        let whole = i64::try_from(numerator.div_euclid(self.d)).ok()?;
2405        let remainder = numerator.rem_euclid(self.d);
2406        #[allow(clippy::cast_precision_loss)]
2407        let fraction = remainder as f64 / self.d as f64;
2408        Some((whole, fraction))
2409    }
2410
2411    /// The interpolation filter, or `None` at unity speed with every position on a source sample.
2412    fn interpolator(&self) -> Option<Interpolator> {
2413        if self.b.unsigned_abs() == self.d.unsigned_abs() && self.a.rem_euclid(self.d) == 0 {
2414            return None;
2415        }
2416        #[allow(clippy::cast_precision_loss)]
2417        let speed = self.b.unsigned_abs() as f64 / self.d.unsigned_abs() as f64;
2418        Some(Interpolator::new(speed))
2419    }
2420
2421    /// The source window `[from, to)` that reading `count` samples from `first_sample` touches,
2422    /// including [`Interpolator`] taps. The map is linear, so the endpoints bound it.
2423    fn request(&self, first_sample: i64, count: usize) -> Option<(i64, i64)> {
2424        let last_sample = first_sample.checked_add(i64::try_from(count).ok()?.checked_sub(1)?)?;
2425        let (first, _) = self.position(first_sample)?;
2426        let (last, _) = self.position(last_sample)?;
2427        let (low, high) = (first.min(last), first.max(last));
2428        let Some(interpolator) = self.interpolator() else {
2429            return Some((low.max(0), high.saturating_add(1)));
2430        };
2431        let half = interpolator.half_taps();
2432        Some((
2433            low.saturating_sub(half - 1).max(0),
2434            high.saturating_add(half + 1),
2435        ))
2436    }
2437
2438    /// A cursor stepping one project sample at a time from `sample`, or `None` if it would not fit.
2439    fn cursor(&self, sample: i64) -> Option<RationalCursor> {
2440        let numerator = self
2441            .a
2442            .checked_add(i128::from(sample).checked_mul(self.b)?)?;
2443        RationalCursor::new(numerator, self.b, self.d)
2444    }
2445
2446    /// What [`Self::position`] returns at the cursor's sample, bit for bit.
2447    fn fraction(&self, cursor: &RationalCursor) -> (i64, f64) {
2448        // The u64 remainder converts to f64 exactly as the i128 one would.
2449        #[allow(clippy::cast_precision_loss)]
2450        let fraction = cursor.remainder as f64 / self.d as f64;
2451        (cursor.whole, fraction)
2452    }
2453}
2454
2455/// Walks `(numerator + n * step) / divisor` over consecutive `n` with additions instead of 128-bit
2456/// divisions. The caller bounds the walk so every floor fits in `i64`.
2457#[derive(Clone, Copy, Debug)]
2458struct RationalCursor {
2459    whole: i64,
2460    remainder: u64,
2461    step_whole: i64,
2462    step_remainder: u64,
2463    divisor: u64,
2464}
2465
2466impl RationalCursor {
2467    fn new(numerator: i128, step: i128, divisor: i128) -> Option<Self> {
2468        // A divisor below 2^63 keeps `remainder + step_remainder` in a u64.
2469        if divisor <= 0 || divisor > i128::from(i64::MAX) {
2470            return None;
2471        }
2472        Some(Self {
2473            whole: i64::try_from(numerator.div_euclid(divisor)).ok()?,
2474            remainder: u64::try_from(numerator.rem_euclid(divisor)).ok()?,
2475            step_whole: i64::try_from(step.div_euclid(divisor)).ok()?,
2476            step_remainder: u64::try_from(step.rem_euclid(divisor)).ok()?,
2477            divisor: u64::try_from(divisor).ok()?,
2478        })
2479    }
2480
2481    /// Steps until the remainder repeats.
2482    const fn period(&self) -> u64 {
2483        let (mut a, mut b) = (self.step_remainder, self.divisor);
2484        while b != 0 {
2485            (a, b) = (b, a % b);
2486        }
2487        self.divisor / a
2488    }
2489
2490    fn advance(&mut self) {
2491        self.whole = self.whole.wrapping_add(self.step_whole);
2492        self.remainder += self.step_remainder;
2493        if self.remainder >= self.divisor {
2494            self.remainder -= self.divisor;
2495            self.whole = self.whole.wrapping_add(1);
2496        }
2497    }
2498}
2499
2500/// [`ticks_at`] for consecutive samples, without a division per sample.
2501fn tick_cursor(sample: i64, sample_rate: u32) -> RationalCursor {
2502    let ticks = i128::from(TICKS_PER_SECOND);
2503    RationalCursor::new(
2504        i128::from(sample) * ticks,
2505        ticks,
2506        i128::from(sample_rate.max(1)),
2507    )
2508    // Unreachable for blocks `render` accepts; saturates like `ticks_at`.
2509    .unwrap_or(RationalCursor {
2510        whole: i64::MAX,
2511        remainder: 0,
2512        step_whole: 0,
2513        step_remainder: 0,
2514        divisor: 1,
2515    })
2516}
2517
2518fn gcd(mut a: i128, mut b: i128) -> i128 {
2519    while b != 0 {
2520        (a, b) = (b, a % b);
2521    }
2522    a.abs().max(1)
2523}
2524
2525/// Project samples between the points where automation is evaluated.
2526///
2527/// About 0.7 ms at 48 kHz: finer than any authored move, coarse enough that keyframe evaluation
2528/// (a cubic Bezier solve) stays cheap.
2529const ENVELOPE_INTERVAL: i64 = 32;
2530
2531/// Writes the `N` multipliers from `at` for `count` samples from `first` into `envelope`.
2532///
2533/// `at` is evaluated at multiples of [`ENVELOPE_INTERVAL`] and interpolated linearly between them.
2534/// An interval containing a knot from `next_knot` (keyframe or fade edge) is evaluated per sample
2535/// instead. Values depend only on the project sample, so block size and seeks never change them.
2536fn fill_envelope<const N: usize>(
2537    envelope: &mut [Vec<f32>],
2538    first: i64,
2539    count: usize,
2540    sample_rate: u32,
2541    next_knot: impl Fn(i64) -> Option<i64>,
2542    at: impl Fn(MediaTime) -> [f32; N],
2543) {
2544    #[allow(clippy::cast_precision_loss)]
2545    let fraction = 1.0 / ENVELOPE_INTERVAL as f32;
2546    // Outer `None` means not yet looked up; inner `None` means no further knot.
2547    let mut knot: Option<Option<i64>> = None;
2548    let mut point: Option<(i64, [f32; N])> = None;
2549    let mut written = 0;
2550    while written < count {
2551        let sample = first.saturating_add(i64::try_from(written).unwrap_or(i64::MAX));
2552        let start = sample.div_euclid(ENVELOPE_INTERVAL) * ENVELOPE_INTERVAL;
2553        let stop = start.saturating_add(ENVELOPE_INTERVAL);
2554        let run = usize::try_from(stop - sample)
2555            .unwrap_or(count)
2556            .clamp(1, count - written);
2557        let (from, to) = (ticks_at(start, sample_rate), ticks_at(stop, sample_rate));
2558        let next = match knot {
2559            Some(next) if next.is_none_or(|next| next > from) => next,
2560            _ => next_knot(from),
2561        };
2562        knot = Some(next);
2563        let outputs = written..written + run;
2564        if next.is_some_and(|next| next <= to) {
2565            let mut ticks = tick_cursor(sample, sample_rate);
2566            for index in outputs {
2567                let value = at(MediaTime::from_ticks(ticks.whole));
2568                ticks.advance();
2569                for (plane, value) in envelope.iter_mut().zip(value) {
2570                    plane[index] = value;
2571                }
2572            }
2573            #[cfg(test)]
2574            ENVELOPE_POINTS.set(ENVELOPE_POINTS.get() + run);
2575        } else {
2576            let left = match point {
2577                Some((at_sample, value)) if at_sample == start => value,
2578                _ => {
2579                    #[cfg(test)]
2580                    ENVELOPE_POINTS.set(ENVELOPE_POINTS.get() + 1);
2581                    at(MediaTime::from_ticks(from))
2582                }
2583            };
2584            let right = at(MediaTime::from_ticks(to));
2585            #[cfg(test)]
2586            ENVELOPE_POINTS.set(ENVELOPE_POINTS.get() + 1);
2587            point = Some((stop, right));
2588            #[allow(clippy::cast_precision_loss)]
2589            let along = (sample - start) as f32;
2590            for (plane, (left, right)) in envelope.iter_mut().zip(left.into_iter().zip(right)) {
2591                let slope = right - left;
2592                for (position, output) in plane[outputs.clone()].iter_mut().enumerate() {
2593                    #[allow(clippy::cast_precision_loss)]
2594                    let position = along + position as f32;
2595                    *output = left + slope * (position * fraction);
2596                }
2597            }
2598        }
2599        written += run;
2600    }
2601}
2602
2603#[cfg(test)]
2604thread_local! {
2605    /// How many times [`fill_envelope`] evaluated an envelope on this thread.
2606    static ENVELOPE_POINTS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
2607    /// Bus channels cleared and summed on this thread.
2608    static BUS_PLANES_CLEARED: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
2609    static BUS_PLANES_SUMMED: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
2610}
2611
2612/// The earlier of two optional ticks, where `None` means never.
2613fn earliest(left: Option<i64>, right: Option<i64>) -> Option<i64> {
2614    match (left, right) {
2615        (Some(left), Some(right)) => Some(left.min(right)),
2616        (left, right) => left.or(right),
2617    }
2618}
2619
2620/// The first keyframe of `animation` after `ticks`; none for a constant or single keyframe.
2621fn next_keyframe(animation: &Animated<f32>, ticks: i64) -> Option<i64> {
2622    match animation {
2623        Animated::Keyframes { keyframes } if keyframes.len() > 1 => {
2624            let next = keyframes.partition_point(|keyframe| keyframe.time.ticks() <= ticks);
2625            keyframes.get(next).map(|keyframe| keyframe.time.ticks())
2626        }
2627        _ => None,
2628    }
2629}
2630
2631/// The first fade start or end after `ticks`.
2632fn next_fade_edge(clip: &ActiveAudioClip, ticks: i64) -> Option<i64> {
2633    let start = clip.timeline_range.start().ticks();
2634    let end = clip.timeline_range.end().ticks();
2635    let fade_in = clip.audio.fade_in.ticks();
2636    let fade_out = clip.audio.fade_out.ticks();
2637    let mut next = None;
2638    if fade_in > 0 {
2639        for edge in [start, start.saturating_add(fade_in)] {
2640            if edge > ticks {
2641                next = earliest(next, Some(edge));
2642            }
2643        }
2644    }
2645    if fade_out > 0 {
2646        for edge in [end.saturating_sub(fade_out), end] {
2647            if edge > ticks {
2648                next = earliest(next, Some(edge));
2649            }
2650        }
2651    }
2652    next
2653}
2654
2655/// The value of a scalar that does not change over time, including a single keyframe.
2656fn constant_value(animation: &Animated<f32>) -> Option<f32> {
2657    match animation {
2658        Animated::Constant { value } => Some(*value),
2659        Animated::Keyframes { keyframes } => match keyframes.as_slice() {
2660            [only] => Some(only.value),
2661            _ => None,
2662        },
2663    }
2664}
2665
2666/// Whether either fade changes the gain in the `count` samples from `first`. Where it does not,
2667/// [`fade_gain`] is exactly one.
2668fn fades_reach(clip: &ActiveAudioClip, first: i64, count: usize, sample_rate: u32) -> bool {
2669    let fade_in = clip.audio.fade_in.ticks();
2670    let fade_out = clip.audio.fade_out.ticks();
2671    if fade_in <= 0 && fade_out <= 0 {
2672        return false;
2673    }
2674    let last = first.saturating_add(i64::try_from(count).unwrap_or(i64::MAX).saturating_sub(1));
2675    // Saturate: pre-roll can read before the clip, and clips may end near the largest tick.
2676    let elapsed = ticks_at(first, sample_rate).saturating_sub(clip.timeline_range.start().ticks());
2677    let remaining = clip
2678        .timeline_range
2679        .end()
2680        .ticks()
2681        .saturating_sub(ticks_at(last, sample_rate));
2682    (fade_in > 0 && elapsed < fade_in) || (fade_out > 0 && remaining < fade_out)
2683}
2684
2685/// The clip's fade multiplier at `time`, using its [`alpha2_model::FadeShape`].
2686fn fade_gain(clip: &ActiveAudioClip, time: MediaTime) -> f32 {
2687    let start = clip.timeline_range.start().ticks();
2688    let end = clip.timeline_range.end().ticks();
2689    let shape = clip.audio.fade_shape;
2690    let mut gain = 1.0_f32;
2691    let fade_in = clip.audio.fade_in.ticks();
2692    if fade_in > 0 {
2693        let elapsed = time.ticks().saturating_sub(start);
2694        if elapsed < fade_in {
2695            gain *= shape.amplitude(ratio(elapsed, fade_in));
2696        }
2697    }
2698    let fade_out = clip.audio.fade_out.ticks();
2699    if fade_out > 0 {
2700        let remaining = end.saturating_sub(time.ticks());
2701        if remaining < fade_out {
2702            gain *= shape.amplitude(ratio(remaining, fade_out));
2703        }
2704    }
2705    gain.clamp(0.0, 1.0)
2706}
2707
2708#[allow(clippy::cast_precision_loss)]
2709fn ratio(value: i64, span: i64) -> f32 {
2710    if span <= 0 {
2711        return 1.0;
2712    }
2713    (value.max(0) as f32 / span as f32).clamp(0.0, 1.0)
2714}
2715
2716/// Constant-power pan weights normalized to unity at the centre, so clip and track pans compose
2717/// without attenuating a default project. A hard pan raises the remaining channel by 3 dB. Mono
2718/// output ignores pan.
2719fn pan_gains(pan: f32, channels: usize) -> [f32; 2] {
2720    if channels < 2 {
2721        return [1.0, 1.0];
2722    }
2723    // Computed in f64 so the centre rounds to exactly 1.0 in f32 (f32 math gives 0.99999994).
2724    let angle = (f64::from(pan.clamp(-1.0, 1.0)) + 1.0) * core::f64::consts::FRAC_PI_4;
2725    let normalize = core::f64::consts::SQRT_2;
2726    #[allow(clippy::cast_possible_truncation)]
2727    let weights = [
2728        (libm::cos(angle) * normalize) as f32,
2729        (libm::sin(angle) * normalize) as f32,
2730    ];
2731    weights
2732}
2733
2734#[cfg(test)]
2735mod tests;