Skip to main content

kithara_audio/
traits.rs

1use std::{
2    num::{NonZeroU32, NonZeroUsize},
3    sync::Arc,
4    time::Duration,
5};
6
7pub use kithara_decode::{DecodeError, DecodeResult};
8use kithara_decode::{PcmChunk, PcmSpec, TrackMetadata};
9use kithara_events::EventBus;
10use kithara_platform::tokio as platform_tokio;
11use platform_tokio::sync::Notify;
12
13mod kithara {
14    pub(crate) use kithara_test_macros::mock;
15}
16
17use crate::ServiceClass;
18
19/// Reason a [`ReadOutcome::Pending`] / [`ChunkOutcome::Pending`] was
20/// returned — i.e. why the reader did not advance this call. Each
21/// variant maps to a distinct caller action; there is no overlap and
22/// no string-matching required.
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24#[non_exhaustive]
25pub enum PendingReason {
26    /// Producer ringbuf is empty: the consumer has caught up to the
27    /// producer's most recent chunk and is waiting for the next one
28    /// (mid-stream async pause, post-seek refill).
29    Buffering,
30    /// A seek was issued; the consumer is waiting for the producer to
31    /// acknowledge the new epoch and deliver post-seek frames. Old
32    /// pre-seek frames have been drained.
33    SeekInProgress,
34    /// Upstream stream-layer surfaced a pending status (network stall,
35    /// retry, source-level backpressure). The reader will progress
36    /// once the stream resumes.
37    StreamBackpressure,
38}
39
40/// Audio processing effect in the chain (transforms PCM chunks).
41#[kithara::mock(api = AudioEffectMock)]
42pub trait AudioEffect: Send + 'static {
43    /// Flush remaining buffered data (called at end of stream).
44    fn flush(&mut self) -> Option<PcmChunk>;
45
46    /// Process a PCM chunk, returning transformed output.
47    ///
48    /// Returns `None` if the effect is accumulating data (not enough for output yet).
49    fn process(&mut self, chunk: PcmChunk) -> Option<PcmChunk>;
50
51    /// Reset internal state (called after seek).
52    fn reset(&mut self);
53}
54
55/// Result of a PCM read.
56///
57/// Each variant carries distinct caller semantics — the type system
58/// guarantees forward progress in `Frames` (via [`NonZeroUsize`]),
59/// while non-progress is explicit in `Pending` with a typed
60/// [`PendingReason`]. Failures surface as `Err(DecodeError)`, never
61/// as an enum variant.
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum ReadOutcome {
64    /// `count` frames were written into the output buffer (`count > 0`
65    /// by construction). `position` is the reader's position
66    /// **after** the read.
67    Frames {
68        count: NonZeroUsize,
69        position: Duration,
70    },
71    /// Reader is alive but produced no frames this call. See
72    /// [`PendingReason`] for the precise cause and required caller
73    /// action. `position` is the reader's current position (it has
74    /// not advanced since the last successful read).
75    Pending {
76        reason: PendingReason,
77        position: Duration,
78    },
79    /// Natural end of stream — the reader played up to `duration()`.
80    /// No more frames will be produced. `position` is the final
81    /// position (usually `duration()`).
82    Eof { position: Duration },
83}
84
85/// Result of a seek — either the reader landed at a known position or
86/// the target was past the known duration. Failures surface as
87/// `Err(DecodeError)`.
88///
89/// `Landed` carries both the requested `target` and the actual
90/// `landed_at`. The two may differ when the underlying decoder
91/// snapped to a granule/segment boundary; callers that want to write
92/// a "post-seek" position should use `landed_at`, not `target`.
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum SeekOutcome {
95    /// Seek completed; reader is now parked at `landed_at`.
96    Landed {
97        target: Duration,
98        landed_at: Duration,
99    },
100    /// Seek target was past the reader's `duration()`. Reader is
101    /// parked at the end; the next `read()` / `next_chunk()` call
102    /// returns `Eof`.
103    PastEof {
104        target: Duration,
105        duration: Duration,
106    },
107}
108
109/// Result of `next_chunk` — either a decoded chunk (with embedded
110/// spec/timing metadata), a typed non-progress signal, or natural
111/// EOF. Failures surface as `Err(DecodeError)`.
112#[derive(Debug)]
113pub enum ChunkOutcome {
114    /// Next decoded chunk.
115    Chunk(PcmChunk),
116    /// Reader is alive but has no chunk ready this tick. See
117    /// [`PendingReason`] for the precise cause; callers may sleep,
118    /// yield, or retry depending on the reason.
119    Pending {
120        reason: PendingReason,
121        position: Duration,
122    },
123    /// Natural end of stream. `position` is the reader's final position.
124    Eof { position: Duration },
125}
126
127/// Primary PCM interface for reading decoded audio.
128///
129/// **Terminal-state contract.** Three failure-mode-agnostic outcomes
130/// are distinguishable by the caller:
131///
132/// - `Ok(ReadOutcome::Frames { .. })` — reader is alive, produced N
133///   frames (possibly 0 if waiting for data).
134/// - `Ok(ReadOutcome::Eof { .. })` — natural end of stream; no more
135///   frames will ever come.
136/// - `Err(DecodeError)` — decoder or channel failure. The reader may
137///   or may not recover; callers that finalise tracks MUST NOT treat
138///   this as EOF.
139///
140/// **Usage pattern:**
141/// ```ignore
142/// // Async preload before audio callback
143/// resource.preload()?;
144///
145/// // In audio callback (non-blocking after preload)
146/// match resource.read_planar(&mut buffers)? {
147///     ReadOutcome::Frames { count, .. } if count > 0 => play_samples(count),
148///     ReadOutcome::Frames { .. } => { /* silence this tick */ }
149///     ReadOutcome::Eof { .. } => finalise_track(),
150/// }
151/// ```
152#[kithara::mock(api = PcmReaderMock)]
153pub trait PcmReader: kithara_platform::MaybeSend {
154    /// Runtime ABR handle for the underlying stream.
155    ///
156    /// Adaptive readers (HLS) return `Some(handle)` so the queue/FFI can
157    /// drive `set_mode` / `set_max_bandwidth_bps` mid-playback. Default
158    /// `None` for non-adaptive readers (file, test fixtures).
159    fn abr_handle(&self) -> Option<kithara_abr::AbrHandle> {
160        None
161    }
162
163    /// Get total duration (if known).
164    fn duration(&self) -> Option<Duration>;
165
166    /// Access the unified event bus for subscribing to all pipeline events.
167    fn event_bus(&self) -> &EventBus;
168
169    /// Get track metadata.
170    fn metadata(&self) -> &TrackMetadata;
171
172    /// Read the next decoded chunk with full metadata.
173    ///
174    /// Returns [`ChunkOutcome::Chunk`] or [`ChunkOutcome::Eof`].
175    /// Decoder / channel failures surface as `Err(DecodeError)`.
176    /// Discards any partially-consumed chunk from previous
177    /// [`PcmReader::read`] calls.
178    ///
179    /// Default implementation reports immediate natural EOF — readers
180    /// without chunk-level support shouldn't be polled this way.
181    ///
182    /// # Errors
183    ///
184    /// Returns `Err(DecodeError)` for terminal producer failures, same
185    /// semantics as [`Self::read`].
186    fn next_chunk(&mut self) -> Result<ChunkOutcome, DecodeError> {
187        Ok(ChunkOutcome::Eof {
188            position: self.position(),
189        })
190    }
191
192    /// Get current playback position.
193    fn position(&self) -> Duration;
194
195    /// Preload initial chunks into internal buffers.
196    ///
197    /// After calling this, subsequent `read()` / `read_planar()` /
198    /// `next_chunk()` return immediately from buffered data without
199    /// blocking. `Err(DecodeError)` is reserved for setup failures
200    /// (e.g. the producer channel closed during preload). Natural EOF
201    /// encountered during preload is **not** surfaced here — the
202    /// subsequent `read` / `next_chunk` will return `Eof`.
203    ///
204    /// # Errors
205    ///
206    /// Returns `Err(DecodeError)` only on terminal setup failure
207    /// (closed PCM channel, backend error). Successful preload always
208    /// returns `Ok(())` even if the stream contains no data.
209    fn preload(&mut self) -> Result<(), DecodeError> {
210        Ok(())
211    }
212
213    /// Get notify for async preload (first chunk available).
214    fn preload_notify(&self) -> Option<Arc<Notify>> {
215        None
216    }
217
218    /// Read interleaved PCM samples.
219    ///
220    /// After `preload()`, returns immediately from buffered data
221    /// without blocking. The returned [`ReadOutcome`] distinguishes
222    /// "produced N frames" (including `count == 0` for transient
223    /// stalls) from natural EOF. Decoder / channel failures surface as
224    /// `Err(DecodeError)`.
225    ///
226    /// # Errors
227    ///
228    /// Returns `Err(DecodeError)` for terminal producer failures:
229    /// closed PCM channel, decoder fault, or backend error. The error
230    /// is one-way — once returned, subsequent reads continue to fail.
231    fn read(&mut self, buf: &mut [f32]) -> Result<ReadOutcome, DecodeError>;
232
233    /// Read deinterleaved (planar) PCM samples.
234    ///
235    /// After `preload()`, returns immediately from buffered data
236    /// without blocking. Each slice in `output` corresponds to one
237    /// channel. The returned [`ReadOutcome`] has the same semantics as
238    /// [`Self::read`]; `count` is frames-per-channel.
239    ///
240    /// # Errors
241    ///
242    /// Same as [`Self::read`] — terminal producer failures are surfaced
243    /// as `Err(DecodeError)`.
244    fn read_planar<'a>(
245        &mut self,
246        output: &'a mut [&'a mut [f32]],
247    ) -> Result<ReadOutcome, DecodeError>;
248
249    /// Seek to the given position.
250    ///
251    /// Returns [`SeekOutcome::Landed`] when the reader is now parked
252    /// at the requested position, [`SeekOutcome::PastEof`] when the
253    /// target was beyond `duration()`. Seek failures (stream I/O,
254    /// decoder recreate) surface as `Err(DecodeError)`.
255    ///
256    /// # Errors
257    ///
258    /// Returns `Err(DecodeError)` when seek cannot complete: stream I/O
259    /// failure, decoder recreate failure, or terminal producer error.
260    fn seek(&mut self, position: Duration) -> Result<SeekOutcome, DecodeError>;
261
262    /// Set the target sample rate of the audio host.
263    ///
264    /// Used for dynamic updates when the host sample rate changes at runtime.
265    fn set_host_sample_rate(&self, _sample_rate: NonZeroU32) {}
266
267    /// Set the playback rate for timeline scaling.
268    ///
269    /// Rate > 1.0 speeds up playback (position advances faster).
270    /// Rate < 1.0 slows down playback (position advances slower).
271    /// The actual pitch-shifting is done by the resampler.
272    fn set_playback_rate(&self, _rate: f32) {}
273
274    /// Update the scheduling priority hint for the shared worker.
275    ///
276    /// Maps track playback state to worker priority: `Audible` tracks
277    /// are decoded first, then `Warm`, then `Idle`.
278    fn set_service_class(&self, _class: ServiceClass) {}
279
280    /// Get the current PCM specification.
281    fn spec(&self) -> PcmSpec;
282}