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)]
11pub struct ActiveAudioClip<'a> {
12 pub clip_id: &'a ClipId,
14 pub source_id: &'a SourceId,
16 pub track_id: &'a TrackId,
18 pub track_order: usize,
20 pub clip_order: usize,
22 pub visual_track: bool,
27 pub timeline_range: TimeRange,
29 pub source_range: TimeRange,
31 pub time_map: TimeMap,
33 pub active_range: TimeRange,
35 pub audio: &'a AudioProperties,
37}
38
39impl ActiveAudioClip<'_> {
40 #[must_use]
48 pub fn speed(&self) -> f64 {
49 match self.time_map {
50 TimeMap::Loop { .. } => return 1.0,
51 TimeMap::Freeze { .. } => return 0.0,
52 TimeMap::Forward | TimeMap::Reverse => {}
53 }
54 let timeline = self.timeline_range.duration().map_or(0, MediaTime::ticks);
55 let source = self.source_range.duration().map_or(0, MediaTime::ticks);
56 if timeline <= 0 || source <= 0 {
57 return 1.0;
58 }
59 #[allow(clippy::cast_precision_loss)]
60 {
61 source as f64 / timeline as f64
62 }
63 }
64
65 #[must_use]
67 pub fn source_time_at(&self, time: MediaTime) -> Option<MediaTime> {
68 crate::map_source_time(
69 self.timeline_range,
70 self.source_range,
71 self.time_map,
72 crate::Mirror::TICK,
73 time,
74 )
75 }
76}
77
78#[derive(Clone, Debug, PartialEq)]
80pub struct AudioSpanDesc<'a> {
81 pub range: TimeRange,
83 pub clips: Vec<ActiveAudioClip<'a>>,
86}
87
88pub fn evaluate_audio<'a>(
107 project: &Project,
108 index: &'a ActiveIndex,
109 range: TimeRange,
110) -> Result<AudioSpanDesc<'a>, EvalError> {
111 crate::ensure_index_matches(project, index)?;
112 let capacity = index.root.map_or(0, |root| {
113 count_overlapping(&index.nodes, root, range, &|entry| entry.source_is_audible)
114 });
115 let mut scratch = AudioSpanScratch {
116 nodes: Vec::with_capacity(capacity),
117 };
118 let span = fill_span(index, range, &mut scratch)?;
119 Ok(AudioSpanDesc {
120 range,
121 clips: span.iter().collect(),
122 })
123}
124
125#[derive(Clone, Debug, Default)]
131pub struct AudioSpanScratch {
132 nodes: Vec<usize>,
133}
134
135#[derive(Clone, Copy, Debug)]
138pub struct AudioSpan<'a, 's> {
139 index: &'a ActiveIndex,
140 range: TimeRange,
141 nodes: &'s [usize],
142}
143
144impl<'a> AudioSpan<'a, '_> {
145 #[must_use]
147 pub const fn range(&self) -> TimeRange {
148 self.range
149 }
150
151 #[must_use]
153 pub const fn len(&self) -> usize {
154 self.nodes.len()
155 }
156
157 #[must_use]
159 pub const fn is_empty(&self) -> bool {
160 self.nodes.is_empty()
161 }
162
163 #[must_use = "iterators are lazy and do nothing unless consumed"]
166 pub fn iter(&self) -> impl ExactSizeIterator<Item = ActiveAudioClip<'a>> {
167 let (index, range) = (self.index, self.range);
168 self.nodes
169 .iter()
170 .map(move |&node| active_clip(index.nodes[node].entry(), range))
171 }
172}
173
174pub fn evaluate_audio_in<'a, 's>(
179 project: &Project,
180 index: &'a ActiveIndex,
181 range: TimeRange,
182 scratch: &'s mut AudioSpanScratch,
183) -> Result<AudioSpan<'a, 's>, EvalError> {
184 crate::ensure_index_matches(project, index)?;
185 fill_span(index, range, scratch)
186}
187
188fn fill_span<'a, 's>(
191 index: &'a ActiveIndex,
192 range: TimeRange,
193 scratch: &'s mut AudioSpanScratch,
194) -> Result<AudioSpan<'a, 's>, EvalError> {
195 let nodes = &mut scratch.nodes;
196 nodes.clear();
197 let mix_order = |entry: &IndexedClip| {
198 (
199 entry.track_order,
200 entry.timeline_range.start(),
201 entry.clip_order,
202 )
203 };
204 let mut unmappable: Option<&IndexedClip> = None;
206 if let Some(root) = index.root {
207 collect_overlapping(&index.nodes, root, range, &mut |node, entry| {
208 if !entry.source_is_audible {
211 return;
212 }
213 let Some(active_range) = entry.timeline_range.intersection(range) else {
214 return;
215 };
216 if active_range.is_empty() {
217 return;
218 }
219 if crate::map_source_time(
221 entry.timeline_range,
222 entry.source_range,
223 entry.time_map,
224 crate::Mirror::TICK,
225 active_range.start(),
226 )
227 .is_none()
228 {
229 if unmappable.is_none_or(|first| mix_order(entry) < mix_order(first)) {
230 unmappable = Some(entry);
231 }
232 return;
233 }
234 nodes.push(node);
235 });
236 }
237 if let Some(entry) = unmappable {
238 return Err(EvalError::UnmappableClip {
239 clip: entry.clip_id.clone(),
240 });
241 }
242 nodes.sort_unstable_by_key(|&node| mix_order(index.nodes[node].entry()));
243 Ok(AudioSpan {
244 index,
245 range,
246 nodes,
247 })
248}
249
250fn active_clip(entry: &IndexedClip, range: TimeRange) -> ActiveAudioClip<'_> {
252 ActiveAudioClip {
253 clip_id: &entry.clip_id,
254 source_id: &entry.source_id,
255 track_id: &entry.track_id,
256 track_order: entry.track_order,
257 clip_order: entry.clip_order,
258 visual_track: entry.track_kind == alpha2_model::TrackKind::Video,
259 timeline_range: entry.timeline_range,
260 source_range: entry.source_range,
261 time_map: entry.time_map,
262 active_range: entry.timeline_range.intersection(range).unwrap_or(range),
263 audio: &entry.audio,
264 }
265}