Skip to main content

pipecrab_audio/
lib.rs

1//! pipecrab-audio: the platform-neutral boundary where audio enters and leaves a
2//! pipeline.
3//!
4//! [`pipecrab-core`](pipecrab_core) owns the *frame* — [`AudioChunk`] and
5//! [`AudioFormat`], re-exported here for convenience. This crate owns *how*
6//! audio crosses the pipeline edge: the [`AudioSource`] and [`AudioSink`] traits
7//! plus hardware-free [`mock`] implementations for tests. Concrete backends
8//! (e.g. cpal for desktop) live in their own crates behind these same traits.
9//!
10//! Audio is a first-party pipeline payload — a typed
11//! [`DataFrame::Audio`](pipecrab_core::DataFrame::Audio), never a `Custom`
12//! frame — so nothing here downcasts. [`Resampler`] defines audio-to-audio
13//! conversion, [`ResamplerStage`] adapts it to the pipeline, and
14//! [`RubatoSincResampler`] provides the bundled windowed-sinc default.
15#![forbid(unsafe_code)]
16#![warn(missing_docs)]
17
18use pipecrab_runtime::{MaybeSend, maybe_async_trait};
19
20pub mod mock;
21mod resampler;
22mod rubato_sinc;
23
24pub use pipecrab_core::{AudioChunk, AudioFormat};
25pub use resampler::{Resampler, ResamplerEffect, ResamplerError, ResamplerStage};
26pub use rubato_sinc::RubatoSincResampler;
27
28/// Why an [`AudioSource`]/[`AudioSink`] (or an underlying backend I/O) failed.
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub enum AudioError {
31    /// The sink starved — no audio was ready when the device needed it.
32    Underrun,
33    /// The device or stream is closed; no more audio can flow.
34    Closed,
35    /// A chunk's format did not match the sink's format. The sink does not
36    /// resample: the caller must feed it chunks in the sink's own format.
37    FormatMismatch {
38        /// The format the sink accepts (its own [`AudioSink::format`]).
39        expected: AudioFormat,
40        /// The format of the rejected chunk.
41        got: AudioFormat,
42    },
43    /// A device- or backend-level failure, with a human-readable description.
44    Device(String),
45}
46
47impl std::fmt::Display for AudioError {
48    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49        match self {
50            AudioError::Underrun => write!(f, "audio underrun"),
51            AudioError::Closed => write!(f, "audio device closed"),
52            AudioError::FormatMismatch { expected, got } => write!(
53                f,
54                "audio format mismatch: sink expects {} Hz/{} ch, got {} Hz/{} ch",
55                expected.sample_rate, expected.channels, got.sample_rate, got.channels,
56            ),
57            AudioError::Device(msg) => write!(f, "audio device error: {msg}"),
58        }
59    }
60}
61
62impl std::error::Error for AudioError {}
63
64// Doc comments live *inside* `maybe_async_trait!` so they attach to the trait
65// (an outer attribute placed before the macro call would document the call, not
66// the item, and be dropped).
67maybe_async_trait! {
68    /// A source of audio flowing *into* a pipeline (device capture, file,
69    /// network, or a [`mock`]).
70    ///
71    /// Carries [`MaybeSend`] like every capability interface: `Send` on native so a
72    /// per-session pump task is spawnable on a multi-threaded executor (a WebRTC
73    /// server runs one source per call), vacuous on `wasm32` where `Send` cannot
74    /// be satisfied. It is `MaybeSend`, not `MaybeSendSync`: [`next_chunk`] takes
75    /// `&mut self`, so the future needs to *move* `self`, never share it. A
76    /// backend whose native handle is `!Send` (cpal's `Stream`) keeps that handle
77    /// off the struct — behind a stream-owning thread — rather than leaking
78    /// `?Send` here.
79    ///
80    /// [`next_chunk`]: AudioSource::next_chunk
81    pub trait AudioSource: MaybeSend {
82        /// The format of the chunks this source yields. Fixed for the source's life.
83        fn format(&self) -> AudioFormat;
84
85        /// Await the next chunk.
86        ///
87        /// Three outcomes, kept distinct so a live device can report failure
88        /// without being mistaken for a clean end of stream:
89        /// - `Ok(Some(chunk))` — a chunk of audio.
90        /// - `Ok(None)` — the source is *gracefully* exhausted (e.g. a file or
91        ///   mock ran out); there will be no more chunks.
92        /// - `Err(_)` — the source *failed* (e.g. the capture device dropped out).
93        async fn next_chunk(&mut self) -> Result<Option<AudioChunk>, AudioError>;
94    }
95}
96
97maybe_async_trait! {
98    /// A sink of audio flowing *out of* a pipeline (device playback, file,
99    /// network, or a [`mock`]).
100    ///
101    /// [`MaybeSend`] for the same reason as [`AudioSource`]: `play` takes
102    /// `&mut self`, so `Send` (native) / vacuous (`wasm32`) is the exact bound.
103    pub trait AudioSink: MaybeSend {
104        /// The format this sink expects incoming chunks to be in.
105        fn format(&self) -> AudioFormat;
106
107        /// Play (or enqueue) one chunk. May `.await` to apply backpressure when
108        /// the sink is full; returns an [`AudioError`] if the chunk cannot be
109        /// accepted.
110        async fn play(&mut self, chunk: AudioChunk) -> Result<(), AudioError>;
111    }
112}