1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
//! [`AudioOutputStream`] — the producer-side trait the
//! speech-to-speech pipeline (`crate::audio::sts`) consumes to push
//! decoded PCM samples at an audio sink.
//!
//! Mirrors the role `AVAudioPlayerNode.scheduleBuffer` plays for
//! Swift's `MLXAudioCore.AudioPlayer.scheduleAudioChunk(_:withCrossfade:)`:
//! the high-level pipeline doesn't care whether the bytes end up on a
//! real device, a file, or a unit-test recorder — it only needs a
//! `write_samples(&[f32])` hook.
//!
//! The trait is intentionally narrow:
//! - [`write_samples`][AudioOutputStream::write_samples] enqueues
//! PCM samples (returns how many it accepted; an `Err` signals a
//! full / closed sink),
//! - [`flush`][AudioOutputStream::flush] signals the producer is done
//! and blocks until the sink has drained,
//! - [`stop`][AudioOutputStream::stop] aborts immediately, dropping
//! any queued samples (the cpal-equivalent of
//! `AVAudioPlayerNode.stop()`),
//! - [`is_running`][AudioOutputStream::is_running] is the single
//! state-introspection hook (mirrors Swift's `isPlaying`).
//!
//! [`super::player::AudioPlayer`] is the default device-backed
//! implementor; pipeline tests can supply a mock via the same
//! trait without pulling in cpal.
use crateResult;
/// A sink that accepts streamed PCM audio frames.
///
/// Implementors are responsible for whatever buffering / format
/// conversion / device handoff they need. The trait contract is the
/// minimum surface the upstream speech-to-speech pipeline
/// ([`crate::audio::sts`]) needs to push decoded PCM at an output:
///
/// - **Sample layout.** `samples` is interleaved PCM at the
/// implementor's negotiated channel count. For a mono stream that
/// means `samples` is a flat `[f32]`; for stereo it's
/// `[L0, R0, L1, R1, …]`. One *frame* = one `channels` group.
/// - **Backpressure.** [`write_samples`] returns `Ok(n)` where `n`
/// may be less than `samples.len()` if the sink could only accept a
/// prefix (the caller is responsible for retrying the remainder).
/// `Err(_)` means the sink rejected the write outright (queue
/// overflow, sink closed, device error).
/// - **Drop semantics.** Dropping an [`AudioOutputStream`] should
/// stop any in-flight playback and release the underlying
/// resources (cpal stream, mutex, etc.). The
/// [`super::player::AudioPlayer`] impl does this via its `Drop`
/// impl.
///
/// `Send` is required so the trait can cross thread boundaries (the
/// speech-to-speech pipeline runs the decoder on a worker thread and pushes
/// samples to the sink without dragging it back to the orchestrator
/// thread). `Sync` is *not* required — write paths are inherently
/// single-producer.
///
/// [`write_samples`]: AudioOutputStream::write_samples
/// Blanket impl forwarding [`AudioOutputStream`] through a mutable
/// reference — lets callers pass `&mut sink` to APIs that accept a
/// `S: AudioOutputStream` by value, retaining ownership for
/// post-call inspection (the [`VoicePipeline::run`] call site this
/// blanket enables).
///
/// The forwarding is a flat delegation; no buffering is added.
///
/// [`VoicePipeline::run`]: crate::audio::sts::pipeline::VoicePipeline::run