1use crate::{ActiveIndex, EvalError, IndexedClip, collect_overlapping, count_overlapping};
2use alpha2_media::SourceId;
3use alpha2_model::{AudioProperties, ClipId, Project, TimeMap, TrackId};
4use alpha2_time::{MediaTime, TimeRange};
5
6#[derive(Clone, Copy, Debug, PartialEq)]
10pub struct ActiveAudioClip<'a> {
11 pub clip_id: &'a ClipId,
13 pub source_id: &'a SourceId,
15 pub track_id: &'a TrackId,
17 pub track_order: usize,
19 pub clip_order: usize,
21 pub visual_track: bool,
23 pub timeline_range: TimeRange,
25 pub source_range: TimeRange,
27 pub time_map: TimeMap,
29 pub active_range: TimeRange,
31 pub audio: &'a AudioProperties,
33}
34
35impl ActiveAudioClip<'_> {
36 #[must_use]
42 pub fn speed(&self) -> f64 {
43 match self.time_map {
44 TimeMap::Loop { .. } => return 1.0,
45 TimeMap::Freeze { .. } => return 0.0,
46 TimeMap::Forward | TimeMap::Reverse => {}
47 }
48 let timeline = self.timeline_range.duration().map_or(0, MediaTime::ticks);
49 let source = self.source_range.duration().map_or(0, MediaTime::ticks);
50 if timeline <= 0 || source <= 0 {
51 return 1.0;
52 }
53 #[allow(clippy::cast_precision_loss)]
54 {
55 source as f64 / timeline as f64
56 }
57 }
58
59 #[must_use]
61 pub fn source_time_at(&self, time: MediaTime) -> Option<MediaTime> {
62 crate::map_source_time(
63 self.timeline_range,
64 self.source_range,
65 self.time_map,
66 crate::Mirror::TICK,
67 time,
68 )
69 }
70}
71
72#[derive(Clone, Debug, PartialEq)]
74pub struct AudioSpanDesc<'a> {
75 pub range: TimeRange,
77 pub clips: Vec<ActiveAudioClip<'a>>,
79}
80
81pub fn evaluate_audio<'a>(
95 project: &Project,
96 index: &'a ActiveIndex,
97 range: TimeRange,
98) -> Result<AudioSpanDesc<'a>, EvalError> {
99 crate::ensure_index_matches(project, index)?;
100 let capacity = index.root.map_or(0, |root| {
101 count_overlapping(&index.nodes, root, range, &|entry| entry.source_is_audible)
102 });
103 let mut scratch = AudioSpanScratch {
104 nodes: Vec::with_capacity(capacity),
105 };
106 let span = fill_span(index, range, &mut scratch)?;
107 Ok(AudioSpanDesc {
108 range,
109 clips: span.iter().collect(),
110 })
111}
112
113#[derive(Clone, Debug, Default)]
117pub struct AudioSpanScratch {
118 nodes: Vec<usize>,
119}
120
121#[derive(Clone, Copy, Debug)]
123pub struct AudioSpan<'a, 's> {
124 index: &'a ActiveIndex,
125 range: TimeRange,
126 nodes: &'s [usize],
127}
128
129impl<'a> AudioSpan<'a, '_> {
130 #[must_use]
132 pub const fn range(&self) -> TimeRange {
133 self.range
134 }
135
136 #[must_use]
138 pub const fn len(&self) -> usize {
139 self.nodes.len()
140 }
141
142 #[must_use]
144 pub const fn is_empty(&self) -> bool {
145 self.nodes.is_empty()
146 }
147
148 #[must_use = "iterators are lazy and do nothing unless consumed"]
150 pub fn iter(&self) -> impl ExactSizeIterator<Item = ActiveAudioClip<'a>> {
151 let (index, range) = (self.index, self.range);
152 self.nodes
153 .iter()
154 .map(move |&node| active_clip(index.nodes[node].entry(), range))
155 }
156}
157
158pub fn evaluate_audio_in<'a, 's>(
163 project: &Project,
164 index: &'a ActiveIndex,
165 range: TimeRange,
166 scratch: &'s mut AudioSpanScratch,
167) -> Result<AudioSpan<'a, 's>, EvalError> {
168 crate::ensure_index_matches(project, index)?;
169 fill_span(index, range, scratch)
170}
171
172fn fill_span<'a, 's>(
173 index: &'a ActiveIndex,
174 range: TimeRange,
175 scratch: &'s mut AudioSpanScratch,
176) -> Result<AudioSpan<'a, 's>, EvalError> {
177 let nodes = &mut scratch.nodes;
178 nodes.clear();
179 let mix_order = |entry: &IndexedClip| {
180 (
181 entry.track_order,
182 entry.timeline_range.start(),
183 entry.clip_order,
184 )
185 };
186 let mut unmappable: Option<&IndexedClip> = None;
187 if let Some(root) = index.root {
188 collect_overlapping(&index.nodes, root, range, &mut |node, entry| {
189 if !entry.source_is_audible {
190 return;
191 }
192 let Some(active_range) = entry.timeline_range.intersection(range) else {
193 return;
194 };
195 if active_range.is_empty() {
196 return;
197 }
198 if crate::map_source_time(
200 entry.timeline_range,
201 entry.source_range,
202 entry.time_map,
203 crate::Mirror::TICK,
204 active_range.start(),
205 )
206 .is_none()
207 {
208 if unmappable.is_none_or(|first| mix_order(entry) < mix_order(first)) {
209 unmappable = Some(entry);
210 }
211 return;
212 }
213 nodes.push(node);
214 });
215 }
216 if let Some(entry) = unmappable {
217 return Err(EvalError::UnmappableClip {
218 clip: entry.clip_id.clone(),
219 });
220 }
221 nodes.sort_unstable_by_key(|&node| mix_order(index.nodes[node].entry()));
222 Ok(AudioSpan {
223 index,
224 range,
225 nodes,
226 })
227}
228
229fn active_clip(entry: &IndexedClip, range: TimeRange) -> ActiveAudioClip<'_> {
230 ActiveAudioClip {
231 clip_id: &entry.clip_id,
232 source_id: &entry.source_id,
233 track_id: &entry.track_id,
234 track_order: entry.track_order,
235 clip_order: entry.clip_order,
236 visual_track: entry.track_kind == alpha2_model::TrackKind::Video,
237 timeline_range: entry.timeline_range,
238 source_range: entry.source_range,
239 time_map: entry.time_map,
240 active_range: entry.timeline_range.intersection(range).unwrap_or(range),
241 audio: &entry.audio,
242 }
243}