aede_core/playback/
exact_session.rs

1//! Continuous, unmodified integer PCM and exact output representations.
2
3use std::error::Error;
4use std::fmt;
5
6use aede_dsp::ProcessStats;
7
8use super::exact_output::{
9    ExactOutputError, ExactOutputFormat, ExactPcmAdapter, MAX_EXACT_BLOCK_FRAMES,
10};
11use super::format::IntegerPcmFormat;
12use super::session::TrackSpan;
13
14/// Failure preparing or producing the unmodified source stream.
15#[derive(Debug)]
16pub enum ExactSessionError {
17    /// Source/output representation or a supplied block is ineligible.
18    Output(ExactOutputError),
19    /// Seal the active source before starting another or finishing.
20    TrackActive,
21    /// Source blocks and endings require an active source occurrence.
22    NoTrack,
23    /// A finished session cannot accept further source audio.
24    Finished,
25    /// The stream exceeded its representable cumulative frame count.
26    FrameCountOverflow,
27}
28
29impl fmt::Display for ExactSessionError {
30    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31        match self {
32            Self::Output(error) => write!(f, "{error}"),
33            Self::TrackActive => f.write_str("seal the active exact playback track first"),
34            Self::NoTrack => f.write_str("no active exact playback track"),
35            Self::Finished => f.write_str("exact playback session has finished"),
36            Self::FrameCountOverflow => f.write_str("exact playback frame count overflowed"),
37        }
38    }
39}
40
41impl Error for ExactSessionError {
42    fn source(&self) -> Option<&(dyn Error + 'static)> {
43        match self {
44            Self::Output(error) => Some(error),
45            _ => None,
46        }
47    }
48}
49
50impl From<ExactOutputError> for ExactSessionError {
51    fn from(error: ExactOutputError) -> Self {
52        Self::Output(error)
53    }
54}
55
56/// Source-depth integers, exact encoded bytes and occurrence attribution.
57pub struct ExactSessionBlock<'a> {
58    /// Unscaled, interleaved source integers; these are never processed.
59    pub samples: &'a [i32],
60    /// The same frames encoded in the session's exact output representation.
61    pub encoded: &'a [u8],
62    /// Shared occurrence spans covering the source samples exactly once.
63    /// Empty completion spans describe source EOF, not device consumption.
64    pub spans: &'a [TrackSpan],
65}
66
67/// A fixed-format source session with no modifying processing stages.
68///
69/// This is a software building block, separate from [`super::session::PcmSession`].
70/// It owns no decoder, transport, sound device or listening records. The existing
71/// driver remains responsible for selection, cancellation/seeking, queue writes
72/// and history after accepted output. There are no gain, tone, rate conversion,
73/// downmix, guard, limiter or dither controls to silently apply or ignore here.
74///
75/// Call `end_track` only after successful decoder EOF, including a present FLAC
76/// MD5 verification. Each occurrence completes once, without delayed frames or
77/// flushing silence. Invalid blocks/lifecycle requests leave state unchanged.
78/// A transport interruption or output failure requires discarding this session
79/// and queued audio. Neither its output metadata nor produced completion markers
80/// establish device eligibility, audible completion or physical bit identity.
81pub struct ExactPcmSession {
82    adapter: ExactPcmAdapter,
83    active: Option<usize>,
84    source_frames: u64,
85    finished: bool,
86    samples: Vec<i32>,
87    bytes: Vec<u8>,
88    spans: Vec<TrackSpan>,
89}
90
91impl ExactPcmSession {
92    /// Validate fixed source/output representation outside the audio callback.
93    pub fn new(
94        input: IntegerPcmFormat,
95        output: ExactOutputFormat,
96    ) -> Result<Self, ExactSessionError> {
97        let adapter = ExactPcmAdapter::new(input, output)?;
98        let capacity = MAX_EXACT_BLOCK_FRAMES * usize::from(input.channels());
99        let mut samples = Vec::new();
100        samples
101            .try_reserve(capacity)
102            .map_err(ExactOutputError::Allocation)?;
103        let mut bytes = Vec::new();
104        bytes
105            .try_reserve(capacity * output.representation().bytes_per_sample())
106            .map_err(ExactOutputError::Allocation)?;
107        let mut spans = Vec::new();
108        spans.try_reserve(1).map_err(ExactOutputError::Allocation)?;
109        Ok(Self {
110            adapter,
111            active: None,
112            source_frames: 0,
113            finished: false,
114            samples,
115            bytes,
116            spans,
117        })
118    }
119
120    /// Original precision, rate and channel association.
121    pub fn input_format(&self) -> IntegerPcmFormat {
122        self.adapter.source_format()
123    }
124
125    /// Exact representation metadata, distinct from device-route evidence.
126    pub fn output_format(&self) -> ExactOutputFormat {
127        self.adapter.output_format()
128    }
129
130    /// Cumulative accepted source frames, with no converted or delayed frames.
131    pub fn source_frames(&self) -> u64 {
132        self.source_frames
133    }
134
135    /// Whether a natural next occurrence can share this fixed-format session.
136    pub fn compatible(&self, input: IntegerPcmFormat, output: ExactOutputFormat) -> bool {
137        !self.finished && input == self.input_format() && output == self.output_format()
138    }
139
140    /// Start a driver-identified occurrence without selecting any processing.
141    pub fn begin_track(&mut self, token: usize) -> Result<(), ExactSessionError> {
142        if self.finished {
143            return Err(ExactSessionError::Finished);
144        }
145        if self.active.is_some() {
146            return Err(ExactSessionError::TrackActive);
147        }
148        self.active = Some(token);
149        self.clear_block();
150        Ok(())
151    }
152
153    /// Preserve one bounded block of complete source frames and encode exactly.
154    ///
155    /// The adapter checks source-depth range, frame alignment and block bounds
156    /// before replacing any output. Observational peaks use an independent
157    /// normalized value; canonical source integers are never altered.
158    pub fn push_source(
159        &mut self,
160        samples: &[i32],
161    ) -> Result<ExactSessionBlock<'_>, ExactSessionError> {
162        if self.finished {
163            return Err(ExactSessionError::Finished);
164        }
165        let token = self.active.ok_or(ExactSessionError::NoTrack)?;
166        let channels = usize::from(self.input_format().channels());
167        let frames = u64::try_from(samples.len() / channels)
168            .map_err(|_| ExactSessionError::FrameCountOverflow)?;
169        let source_frames = self
170            .source_frames
171            .checked_add(frames)
172            .ok_or(ExactSessionError::FrameCountOverflow)?;
173        self.adapter.encode_block(samples, &mut self.bytes)?;
174        self.samples.clear();
175        self.samples.extend_from_slice(samples);
176        self.spans.clear();
177        if !samples.is_empty() {
178            // Every admitted 16/24-bit integer is exactly representable at
179            // this power-of-two scale. This view observes, never processes.
180            let scale = if self.input_format().bits_per_sample() == 16 {
181                1.0 / 32768.0
182            } else {
183                1.0 / 8388608.0
184            };
185            let sample_peak = samples
186                .iter()
187                .map(|&sample| (sample as f32 * scale).abs())
188                .fold(0.0, f32::max);
189            self.spans.push(TrackSpan {
190                token,
191                samples: 0..samples.len(),
192                stats: ProcessStats {
193                    sample_peak,
194                    overfull_samples: 0,
195                },
196                complete: false,
197            });
198        }
199        self.source_frames = source_frames;
200        Ok(self.block())
201    }
202
203    /// Seal one successfully decoded source, with no tail or extra samples.
204    pub fn end_track(&mut self) -> Result<ExactSessionBlock<'_>, ExactSessionError> {
205        if self.finished {
206            return Err(ExactSessionError::Finished);
207        }
208        let token = self.active.ok_or(ExactSessionError::NoTrack)?;
209        self.clear_block();
210        self.active = None;
211        self.spans.push(TrackSpan {
212            token,
213            samples: 0..0,
214            stats: ProcessStats::default(),
215            complete: true,
216        });
217        Ok(self.block())
218    }
219
220    /// Finish the sealed group; repeated calls emit no duplicate completion.
221    pub fn finish(&mut self) -> Result<ExactSessionBlock<'_>, ExactSessionError> {
222        if self.active.is_some() {
223            return Err(ExactSessionError::TrackActive);
224        }
225        self.clear_block();
226        self.finished = true;
227        Ok(self.block())
228    }
229
230    fn clear_block(&mut self) {
231        self.samples.clear();
232        self.bytes.clear();
233        self.spans.clear();
234    }
235
236    fn block(&self) -> ExactSessionBlock<'_> {
237        ExactSessionBlock {
238            samples: &self.samples,
239            encoded: &self.bytes,
240            spans: &self.spans,
241        }
242    }
243}
244
245#[cfg(test)]
246#[path = "exact_session_tests.rs"]
247mod tests;