tellur-core 0.3.0

Core component model for tellur: vector/raster/timeline components, layout, easing, and events
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
//! Integer-frame requests and caller-owned output blocks for recursive audio
//! rendering.
//!
//! A request keeps its root-timeline span as integer sample frames while an
//! affine `f64` mapping describes the local time seen by the current component.
//! Wrappers transform that mapping directly, so evaluating a sample never
//! depends on where a caller happened to split the surrounding render blocks.

use std::collections::HashMap;
use std::sync::Arc;

use super::AudioBuffer;

/// One half-open block request for recursive audio rendering.
///
/// `start_frame..start_frame + frame_count` is always expressed on the root
/// output sample grid. `local_start` is the component-local time, in seconds,
/// at `start_frame`; `local_step` is the number of local seconds advanced by
/// one root output frame.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct AudioRenderRequest {
    start_frame: i64,
    frame_count: usize,
    rate: u32,
    channels: u16,
    local_start: f64,
    local_step: f64,
}

impl AudioRenderRequest {
    /// Creates a root request whose local clock is the root output clock.
    pub fn new(start_frame: i64, frame_count: usize, rate: u32, channels: u16) -> Self {
        assert!(rate > 0, "audio sample rate must be non-zero");
        assert!(channels > 0, "audio channel count must be non-zero");

        let local_step = 1.0 / f64::from(rate);
        let local_start = start_frame as f64 * local_step;
        let request = Self {
            start_frame,
            frame_count,
            rate,
            channels,
            local_start,
            local_step,
        };
        // Fail at construction rather than much later when a buffer is sized.
        let _ = request.sample_len();
        request
    }

    /// The first requested frame on the root output sample grid.
    pub fn start_frame(self) -> i64 {
        self.start_frame
    }

    /// The number of requested root output frames.
    pub fn frame_count(self) -> usize {
        self.frame_count
    }

    /// The root output sample rate, in Hz.
    pub fn rate(self) -> u32 {
        self.rate
    }

    /// The number of interleaved output channels.
    pub fn channels(self) -> u16 {
        self.channels
    }

    /// Component-local time, in seconds, at the request's first frame.
    pub fn local_start(self) -> f64 {
        self.local_start
    }

    /// Component-local seconds advanced by one root output frame.
    pub fn local_step(self) -> f64 {
        self.local_step
    }

    /// The exact number of interleaved `f32` samples required by this request.
    pub fn sample_len(self) -> usize {
        self.frame_count
            .checked_mul(usize::from(self.channels))
            .expect("audio block sample count overflow")
    }

    /// Returns the component-local time at `frame_offset` within this request.
    ///
    /// The value is derived directly from the affine mapping on every call; no
    /// incrementally accumulated clock is involved. `frame_offset ==
    /// frame_count` is allowed so callers can evaluate the half-open end.
    pub fn time_at(self, frame_offset: usize) -> f64 {
        assert!(
            frame_offset <= self.frame_count,
            "audio frame offset {frame_offset} exceeds request length {}",
            self.frame_count
        );
        (frame_offset as f64).mul_add(self.local_step, self.local_start)
    }

    /// Whether at least one requested sample may lie in `[start, end)` on the
    /// current component's local clock.
    ///
    /// This conservative endpoint test lets containers skip whole inactive
    /// children before they decode or allocate effect state. It never skips a
    /// block that could contribute, including for a reversed affine clock.
    pub fn may_overlap_local(self, start: f64, end: f64) -> bool {
        if self.frame_count == 0 || !start.is_finite() || !end.is_finite() || end <= start {
            return false;
        }
        let first = self.time_at(0);
        let last = self.time_at(self.frame_count - 1);
        let (earliest, latest) = if first <= last {
            (first, last)
        } else {
            (last, first)
        };
        earliest < end && latest >= start
    }

    /// Replaces the affine local-time mapping without changing the root span or
    /// output format.
    pub fn with_local_timing(mut self, local_start: f64, local_step: f64) -> Self {
        assert!(local_start.is_finite(), "audio local start must be finite");
        assert!(local_step.is_finite(), "audio local step must be finite");
        self.local_start = local_start;
        self.local_step = local_step;
        self
    }

    /// Translates the component-local clock by `delta` seconds.
    pub fn shift_local(mut self, delta: f64) -> Self {
        assert!(delta.is_finite(), "audio local translation must be finite");
        self.local_start += delta;
        assert!(
            self.local_start.is_finite(),
            "translated audio local start must be finite"
        );
        self
    }

    /// Alias for [`shift_local`](Self::shift_local), phrased as a time transform.
    pub fn translated(self, delta: f64) -> Self {
        self.shift_local(delta)
    }

    /// Alias for [`with_local_timing`](Self::with_local_timing), phrased as an
    /// affine remap.
    pub fn remapped(self, local_start: f64, local_step: f64) -> Self {
        self.with_local_timing(local_start, local_step)
    }

    /// Selects a frame subrange and advances both the root and local starts.
    pub fn subrange(self, frame_offset: usize, frame_count: usize) -> Self {
        let end = frame_offset
            .checked_add(frame_count)
            .expect("audio subrange overflow");
        assert!(
            end <= self.frame_count,
            "audio subrange {frame_offset}..{end} exceeds request length {}",
            self.frame_count
        );
        let start_delta = i64::try_from(frame_offset).expect("audio frame offset exceeds i64");
        Self {
            start_frame: self
                .start_frame
                .checked_add(start_delta)
                .expect("audio root frame overflow"),
            frame_count,
            local_start: self.time_at(frame_offset),
            ..self
        }
    }

    /// Expands the request by root output frames on both sides.
    ///
    /// The local affine mapping is extended backwards and forwards unchanged,
    /// which makes this suitable for finite filter halos and resampler kernels.
    pub fn expanded(self, left_frames: usize, right_frames: usize) -> Self {
        let left = i64::try_from(left_frames).expect("left audio halo exceeds i64");
        let frame_count = self
            .frame_count
            .checked_add(left_frames)
            .and_then(|count| count.checked_add(right_frames))
            .expect("expanded audio request length overflow");
        let local_start = (-(left_frames as f64)).mul_add(self.local_step, self.local_start);
        let expanded = Self {
            start_frame: self
                .start_frame
                .checked_sub(left)
                .expect("expanded audio root frame underflow"),
            frame_count,
            local_start,
            ..self
        };
        let _ = expanded.sample_len();
        expanded
    }
}

/// A caller-owned interleaved PCM block paired with its render request.
///
/// A callee must overwrite the entire block, including writing zeroes where it
/// contributes silence. The constructor enforces the request/buffer shape.
pub struct AudioBlockMut<'a> {
    request: AudioRenderRequest,
    samples: &'a mut [f32],
}

impl<'a> AudioBlockMut<'a> {
    /// Pairs `samples` with `request`, panicking when their shapes differ.
    pub fn new(request: AudioRenderRequest, samples: &'a mut [f32]) -> Self {
        assert_eq!(
            samples.len(),
            request.sample_len(),
            "audio block must contain frame_count * channels interleaved samples"
        );
        Self { request, samples }
    }

    /// The request represented by this block.
    pub fn request(&self) -> AudioRenderRequest {
        self.request
    }

    /// The interleaved PCM samples.
    pub fn samples(&self) -> &[f32] {
        self.samples
    }

    /// Mutable access to the interleaved PCM samples.
    pub fn samples_mut(&mut self) -> &mut [f32] {
        self.samples
    }

    /// Fills the entire block with silence.
    pub fn clear(&mut self) {
        self.samples.fill(0.0);
    }

    /// The number of interleaved samples in the block.
    pub fn len(&self) -> usize {
        self.samples.len()
    }

    /// Whether the block contains no interleaved samples.
    pub fn is_empty(&self) -> bool {
        self.samples.is_empty()
    }
}

/// Per-render scratch storage passed through recursive audio evaluation.
///
/// Scratch vectors are returned by value so a component may keep one while it
/// recursively re-borrows the context for its child.
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
struct ConformedSourceMemoKey {
    path: String,
    rate: u32,
    channels: u16,
    gain_bits: u32,
}

#[derive(Debug, Default)]
pub struct AudioRenderContext {
    scratch: Vec<Vec<f32>>,
    source_durations: HashMap<String, Option<f64>>,
    conformed_sources: HashMap<ConformedSourceMemoKey, Option<Arc<AudioBuffer>>>,
}

impl AudioRenderContext {
    /// Creates an empty render context.
    pub fn new() -> Self {
        Self::default()
    }

    /// Takes a zero-filled scratch vector of exactly `len` samples.
    pub fn take_scratch(&mut self, len: usize) -> Vec<f32> {
        let mut scratch = self.scratch.pop().unwrap_or_default();
        scratch.resize(len, 0.0);
        scratch.fill(0.0);
        scratch
    }

    /// Returns a scratch vector for reuse by a later recursive call.
    pub fn recycle_scratch(&mut self, mut scratch: Vec<f32>) {
        scratch.clear();
        self.scratch.push(scratch);
    }

    /// Returns a source's native duration, memoizing both decode success and
    /// failure for this render traversal.
    pub(crate) fn source_duration(&mut self, path: &str) -> Option<f64> {
        self.source_duration_with(path, || crate::audio::decoded_duration(path, None).ok())
    }

    fn source_duration_with<F>(&mut self, path: &str, load: F) -> Option<f64>
    where
        F: FnOnce() -> Option<f64>,
    {
        if let Some(duration) = self.source_durations.get(path) {
            return *duration;
        }

        let duration = load();
        self.source_durations.insert(path.to_owned(), duration);
        duration
    }

    /// Returns source PCM conformed to the root output format, retaining the
    /// resulting buffer even when the process-wide best-effort cache declines
    /// it. Decode failures are retained too, so later blocks stay silent
    /// without retrying the same source.
    pub(crate) fn conformed_source(
        &mut self,
        path: &str,
        rate: u32,
        channels: u16,
        gain: f32,
    ) -> Option<Arc<AudioBuffer>> {
        let key = ConformedSourceMemoKey {
            path: path.to_owned(),
            rate,
            channels,
            gain_bits: gain.to_bits(),
        };
        self.conformed_source_with(key, || {
            crate::audio::conform_file_cached(path, None, rate, channels, gain, 1.0).ok()
        })
    }

    fn conformed_source_with<F>(
        &mut self,
        key: ConformedSourceMemoKey,
        load: F,
    ) -> Option<Arc<AudioBuffer>>
    where
        F: FnOnce() -> Option<Arc<AudioBuffer>>,
    {
        if let Some(source) = self.conformed_sources.get(&key) {
            return source.clone();
        }

        let source = load();
        self.conformed_sources.insert(key, source.clone());
        source
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn large_offset_keeps_adjacent_48khz_samples_distinct() {
        // Ten hours is far beyond the point where f32 can represent adjacent
        // absolute 48 kHz sample indices, while f64 retains ample precision.
        let start_frame = 48_000_i64 * 60 * 60 * 10;
        let request = AudioRenderRequest::new(start_frame, 2, 48_000, 2);

        let first = request.time_at(0);
        let second = request.time_at(1);
        let sample_period = 1.0 / 48_000.0;

        assert_ne!(first, second);
        assert!(((second - first) - sample_period).abs() < 1.0e-11);
        assert_eq!(first, start_frame as f64 / 48_000.0);
    }

    #[test]
    fn subrange_and_expansion_preserve_the_affine_mapping() {
        let request = AudioRenderRequest::new(100, 20, 1_000, 2).with_local_timing(5.0, 0.002);
        let subrange = request.subrange(4, 6);

        assert_eq!(subrange.start_frame(), 104);
        assert_eq!(subrange.frame_count(), 6);
        assert_eq!(subrange.local_start(), request.time_at(4));
        assert_eq!(subrange.time_at(6), request.time_at(10));

        let expanded = subrange.expanded(2, 3);
        assert_eq!(expanded.start_frame(), 102);
        assert_eq!(expanded.frame_count(), 11);
        assert!((expanded.local_start() - request.time_at(2)).abs() < 1.0e-14);
        assert!((expanded.time_at(11) - request.time_at(13)).abs() < 1.0e-14);

        let shifted = expanded.shift_local(1.25);
        assert_eq!(shifted.start_frame(), expanded.start_frame());
        assert_eq!(shifted.local_step(), expanded.local_step());
        assert_eq!(shifted.time_at(5), expanded.time_at(5) + 1.25);

        let remapped = shifted.remapped(-3.0, 0.5);
        assert_eq!(remapped.time_at(0), -3.0);
        assert_eq!(remapped.time_at(2), -2.0);
    }

    #[test]
    fn overlap_test_respects_half_open_local_intervals() {
        let request = AudioRenderRequest::new(10, 4, 10, 1);
        assert!(request.may_overlap_local(1.0, 1.1));
        assert!(request.may_overlap_local(1.2, 2.0));
        assert!(!request.may_overlap_local(0.0, 1.0));
        assert!(!request.may_overlap_local(1.4, 2.0));
        assert!(!request.subrange(0, 0).may_overlap_local(0.0, 2.0));
    }

    #[test]
    fn audio_block_enforces_shape_and_exposes_samples() {
        let request = AudioRenderRequest::new(0, 3, 48_000, 2);
        assert_eq!(request.sample_len(), 6);

        let mut samples = vec![1.0_f32; request.sample_len()];
        let mut block = AudioBlockMut::new(request, &mut samples);
        assert_eq!(block.request(), request);
        assert_eq!(block.len(), 6);
        assert!(!block.is_empty());
        block.samples_mut()[1] = 0.5;
        assert_eq!(block.samples()[1], 0.5);
        block.clear();
        assert!(block.samples().iter().all(|sample| *sample == 0.0));
    }

    #[test]
    #[should_panic(expected = "audio block must contain frame_count * channels")]
    fn audio_block_rejects_wrong_interleaved_length() {
        let request = AudioRenderRequest::new(0, 3, 48_000, 2);
        let mut samples = vec![0.0_f32; 5];
        let _ = AudioBlockMut::new(request, &mut samples);
    }

    #[test]
    fn render_context_recycles_zeroed_scratch() {
        let mut context = AudioRenderContext::new();
        let mut scratch = context.take_scratch(4);
        scratch.fill(1.0);
        let capacity = scratch.capacity();
        context.recycle_scratch(scratch);

        let reused = context.take_scratch(3);
        assert!(reused.capacity() >= capacity);
        assert_eq!(reused, vec![0.0; 3]);
    }

    #[test]
    fn render_context_memoizes_source_duration_success_and_failure() {
        let mut context = AudioRenderContext::new();

        assert_eq!(
            context.source_duration_with("voice.wav", || Some(2.5)),
            Some(2.5)
        );
        assert_eq!(
            context.source_duration_with("voice.wav", || panic!("duration reloaded")),
            Some(2.5)
        );

        assert_eq!(context.source_duration_with("missing.wav", || None), None);
        assert_eq!(
            context.source_duration_with("missing.wav", || panic!("failure retried")),
            None
        );
    }

    #[test]
    fn render_context_memoizes_conformed_source_success_and_failure() {
        let mut context = AudioRenderContext::new();
        let key = ConformedSourceMemoKey {
            path: "voice.wav".to_owned(),
            rate: 48_000,
            channels: 2,
            gain_bits: 0.5_f32.to_bits(),
        };
        let source = Arc::new(AudioBuffer {
            samples: vec![0.25, 0.25],
            rate: 48_000,
            channels: 2,
        });

        let first = context
            .conformed_source_with(key.clone(), || Some(Arc::clone(&source)))
            .expect("first load succeeds");
        let second = context
            .conformed_source_with(key, || panic!("source reloaded"))
            .expect("memoized load succeeds");
        assert!(Arc::ptr_eq(&first, &second));

        let missing_key = ConformedSourceMemoKey {
            path: "missing.wav".to_owned(),
            rate: 48_000,
            channels: 2,
            gain_bits: 1.0_f32.to_bits(),
        };
        assert!(context
            .conformed_source_with(missing_key.clone(), || None)
            .is_none());
        assert!(context
            .conformed_source_with(missing_key, || panic!("failure retried"))
            .is_none());
    }
}