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}