Skip to main content

tono_core/runtime/
source.rs

1//! The crate's output seam: the [`AudioSource`] trait every adapter targets,
2//! the interleaved-stereo channel spread, and the allocation-free
3//! [`StreamSource`] over a streamable doc.
4
5use super::SCRATCH_FRAMES;
6use crate::dsl::SoundDoc;
7use crate::streaming::StreamGraph;
8
9/// A block-serving audio source: fill `out` (interleaved stereo L,R,L,R…) and
10/// return the number of frames written. Runs indefinitely. This is the single
11/// seam host output adapters target, so a `cpal` callback, an AudioWorklet, or a
12/// Bevy source never depend on a concrete engine type.
13///
14/// Implementations overwrite the **whole** `out` buffer (silence where there is
15/// nothing to play), so a caller may mix several sources through one scratch
16/// buffer without re-zeroing.
17pub trait AudioSource {
18    /// Fill `out` with the next block of interleaved-stereo audio.
19    fn fill(&mut self, out: &mut [f32]) -> usize;
20
21    /// Rewind the source to its start (playback position / phase to zero).
22    /// Defaults to a no-op; a looping source overrides it so a transport can
23    /// restart it from the top. [`AdaptiveMusic::reset`](crate::adaptive::AdaptiveMusic::reset)
24    /// calls this on each layer.
25    fn reset(&mut self) {}
26}
27
28/// Spread an interleaved-stereo buffer across a device's channel layout: mono
29/// devices get the mid (`0.5 * (l + r)`), stereo gets L/R, extra channels are
30/// zeroed. The one channel-adaptation every output adapter (cpal callback,
31/// AudioWorklet shim) needs — pure sample shuffling, no device dependency.
32/// `data` holds `channels` interleaved device channels; `stereo` holds the same
33/// frame count as L,R pairs.
34pub fn write_interleaved(data: &mut [f32], channels: usize, stereo: &[f32]) {
35    let channels = channels.max(1);
36    // Never read past the source: a caller handing a short `stereo` slice must
37    // not panic on the audio thread — fill only the frames we actually have.
38    let frames = (data.len() / channels).min(stereo.len() / 2);
39    for f in 0..frames {
40        let (l, r) = (stereo[f * 2], stereo[f * 2 + 1]);
41        let base = f * channels;
42        if channels == 1 {
43            data[base] = 0.5 * (l + r);
44        } else {
45            data[base] = l;
46            data[base + 1] = r;
47            for c in 2..channels {
48                data[base + c] = 0.0;
49            }
50        }
51    }
52}
53/// An [`AudioSource`] over the stateful streaming renderer: streams a
54/// doc's graph **indefinitely** — mono duplicated to stereo. Returns `None`
55/// for docs outside the streamable subset (the caller falls back to a
56/// buffer-backed [`Player`](crate::player::Player)/instance). This is how a game
57/// feeds the streaming renderer straight to a cpal / AudioWorklet callback
58/// for continuous generative content.
59///
60/// `fill` is allocation-free for blocks up to 8192 frames (the scratch is
61/// pre-allocated at construction); a larger block grows it once, on the first
62/// such call. Caveats past the document's `duration`: a `seq` yields silence
63/// past its pre-rendered buffer (only oscillators/noise truly run forever),
64/// and the baked peak-limit gain was measured over the document only —
65/// free-running output past `duration` is un-limited and can clip where the
66/// finite bounce could not.
67///
68/// Byte-identity: the offline bounce ends in a transparent sample-peak safety
69/// limit, a whole-buffer gain that cannot be computed causally. [`StreamSource::from_doc`] therefore measures the finite render's peak with
70/// one throwaway pass of the same deterministic graph (O(duration) time, O(1)
71/// memory) and bakes the identical constant gain, so the stream matches the
72/// bounce bit-for-bit over the document's duration.
73pub struct StreamSource {
74    graph: StreamGraph,
75    scratch: Vec<f32>,
76    /// The bounce's peak-limit gain (1.0 when the doc never exceeds the ceiling).
77    gain: f32,
78}
79
80impl StreamSource {
81    /// Build a streaming source for `doc`, or `None` if it isn't streamable.
82    pub fn from_doc(doc: &SoundDoc) -> Option<Self> {
83        let graph = StreamGraph::try_from_doc(doc)?;
84        // Probe pass: same graph, same bytes — find the peak the offline
85        // output stage would have limited against. The duration clamp mirrors
86        // the offline render paths so an unvalidated doc can't request an
87        // unbounded probe (or seq pre-render) here.
88        let mut probe = StreamGraph::try_from_doc(doc)?;
89        let mut remaining =
90            ((doc.duration.clamp(0.0, 600.0) * doc.sample_rate as f32).ceil() as usize).max(1);
91        let mut block = [0.0f32; 1024];
92        let mut peak = 0.0f32;
93        while remaining > 0 {
94            let take = block.len().min(remaining);
95            probe.fill(&mut block[..take]);
96            peak = block[..take].iter().fold(peak, |m, x| m.max(x.abs()));
97            remaining -= take;
98        }
99        let gain = if peak > crate::dsp::CEIL {
100            crate::dsp::CEIL / peak
101        } else {
102            1.0
103        };
104        Some(StreamSource {
105            graph,
106            // Pre-sized so common host blocks never allocate in `fill`.
107            scratch: vec![0.0; SCRATCH_FRAMES],
108            gain,
109        })
110    }
111}
112
113impl AudioSource for StreamSource {
114    fn fill(&mut self, out: &mut [f32]) -> usize {
115        let frames = out.len() / 2;
116        if self.scratch.len() < frames {
117            self.scratch.resize(frames, 0.0);
118        }
119        let mono = &mut self.scratch[..frames];
120        self.graph.fill(mono);
121        for f in 0..frames {
122            let v = mono[f] * self.gain;
123            out[f * 2] = v;
124            out[f * 2 + 1] = v;
125        }
126        frames
127    }
128}