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, or the real
55/// stereo bus of a schema-v2 `tracks` mixer. Returns `None`
56/// for docs outside the streamable subset (the caller falls back to a
57/// buffer-backed [`Player`](crate::player::Player)/instance). This is how a game
58/// feeds the streaming renderer straight to a cpal / AudioWorklet callback
59/// for continuous generative content.
60///
61/// `fill` is allocation-free for blocks up to 8192 frames (the scratch is
62/// pre-allocated at construction); a larger block grows it once, on the first
63/// such call. Caveats past the document's `duration`: a `seq` yields silence
64/// past its pre-rendered buffer (only oscillators/noise truly run forever),
65/// and the baked peak-limit gain was measured over the document only —
66/// free-running output past `duration` is un-limited and can clip where the
67/// finite bounce could not.
68///
69/// Byte-identity: the offline bounce ends in a transparent sample-peak safety
70/// limit, a whole-buffer gain that cannot be computed causally. [`StreamSource::from_doc`] therefore measures the finite render's peak with
71/// one throwaway pass of the same deterministic graph (O(duration) time, O(1)
72/// memory) and bakes the identical constant gain, so the stream matches the
73/// bounce bit-for-bit over the document's duration. A `tracks` document is
74/// probed over BOTH channels — the offline limits the stereo bus jointly.
75pub struct StreamSource {
76    graph: StreamGraph,
77    scratch: Vec<f32>,
78    /// Right-channel scratch for a stereo (`tracks`) document.
79    scratch_r: Vec<f32>,
80    /// The bounce's peak-limit gain (1.0 when the doc never exceeds the ceiling).
81    gain: f32,
82    /// Whether the doc renders a real stereo image (a `tracks` mixer).
83    stereo: bool,
84}
85
86impl StreamSource {
87    /// Build a streaming source for `doc`, or `None` if it isn't streamable.
88    pub fn from_doc(doc: &SoundDoc) -> Option<Self> {
89        let graph = StreamGraph::try_from_doc(doc)?;
90        let stereo = graph.is_stereo();
91        // Probe pass: same graph, same bytes — find the peak the offline
92        // output stage would have limited against. The duration clamp mirrors
93        // the offline render paths so an unvalidated doc can't request an
94        // unbounded probe (or seq pre-render) here.
95        let mut probe = StreamGraph::try_from_doc(doc)?;
96        let mut remaining =
97            ((doc.duration.clamp(0.0, 600.0) * doc.sample_rate as f32).ceil() as usize).max(1);
98        let mut block = [0.0f32; 1024];
99        let mut block_r = [0.0f32; 1024];
100        let mut peak = 0.0f32;
101        while remaining > 0 {
102            let take = block.len().min(remaining);
103            if stereo {
104                probe.fill_stereo(&mut block[..take], &mut block_r[..take]);
105                peak = block[..take]
106                    .iter()
107                    .chain(block_r[..take].iter())
108                    .fold(peak, |m, x| m.max(x.abs()));
109            } else {
110                probe.fill(&mut block[..take]);
111                peak = block[..take].iter().fold(peak, |m, x| m.max(x.abs()));
112            }
113            remaining -= take;
114        }
115        let gain = if peak > crate::dsp::CEIL {
116            crate::dsp::CEIL / peak
117        } else {
118            1.0
119        };
120        Some(StreamSource {
121            graph,
122            // Pre-sized so common host blocks never allocate in `fill`.
123            scratch: vec![0.0; SCRATCH_FRAMES],
124            scratch_r: if stereo {
125                vec![0.0; SCRATCH_FRAMES]
126            } else {
127                Vec::new()
128            },
129            gain,
130            stereo,
131        })
132    }
133}
134
135impl AudioSource for StreamSource {
136    fn fill(&mut self, out: &mut [f32]) -> usize {
137        let frames = out.len() / 2;
138        if self.scratch.len() < frames {
139            self.scratch.resize(frames, 0.0);
140        }
141        let gain = self.gain;
142        if self.stereo {
143            if self.scratch_r.len() < frames {
144                self.scratch_r.resize(frames, 0.0);
145            }
146            self.graph
147                .fill_stereo(&mut self.scratch[..frames], &mut self.scratch_r[..frames]);
148            for f in 0..frames {
149                out[f * 2] = self.scratch[f] * gain;
150                out[f * 2 + 1] = self.scratch_r[f] * gain;
151            }
152        } else {
153            let mono = &mut self.scratch[..frames];
154            self.graph.fill(mono);
155            for f in 0..frames {
156                let v = mono[f] * gain;
157                out[f * 2] = v;
158                out[f * 2 + 1] = v;
159            }
160        }
161        frames
162    }
163}