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
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
//! Rubato conversion and final input drain.
use super::*;
impl ResamplerState {
/// Create a fixed-ratio resampler with `channels` channels from `in_sr` to `out_sr`.
///
/// Returns [`Error::Backend`] if rubato construction fails (avoids a panic silently stopping
/// the thread).
pub(super) fn new(in_sr: u32, out_sr: u32, channels: usize) -> Result<Self> {
let ratio = out_sr as f64 / in_sr as f64;
// Fixed input chunk of about 20ms worth of input frames (rubato retains any remainder internally).
let chunk_in_frames = (in_sr as usize / 50).max(64);
let params = SincInterpolationParameters {
sinc_len: 128,
f_cutoff: 0.95,
interpolation: SincInterpolationType::Linear,
oversampling_factor: 128,
window: WindowFunction::BlackmanHarris2,
};
let inner = Async::<f32>::new_sinc(
ratio,
1.0, // Fixed ratio (no variable resampling needed)
¶ms,
chunk_in_frames,
channels,
FixedAsync::Input,
)
.map_err(|e| Error::Backend(format!("rubato sinc resampler construction failed: {e}")))?;
let max_out_frames = inner.output_frames_max();
Ok(Self {
inner,
channels,
chunk_in_frames,
max_out_frames,
in_accum: Vec::with_capacity(chunk_in_frames * channels * 4),
out_scratch: vec![0.0; max_out_frames * channels],
})
}
/// Resample as much of `in_accum` as possible in `chunk_in_frames` units and append the
/// resulting interleaved samples to `out_buf`. Returns the number of output frames generated.
///
/// Returns [`Error::Backend`] if constructing a rubato adapter or calling `process_into_buffer`
/// fails (avoids a panic silently stopping the capture thread).
pub(super) fn drain_into(&mut self, out_buf: &mut Vec<f32>) -> Result<u64> {
let step = self.chunk_in_frames * self.channels;
let mut produced = 0u64;
while self.in_accum.len() >= step {
let in_adapter =
InterleavedSlice::new(&self.in_accum[..step], self.channels, self.chunk_in_frames)
.map_err(|e| {
Error::Backend(format!("rubato interleaved input adapter failed: {e}"))
})?;
let mut out_adapter = InterleavedSlice::new_mut(
&mut self.out_scratch[..],
self.channels,
self.max_out_frames,
)
.map_err(|e| {
Error::Backend(format!("rubato interleaved output adapter failed: {e}"))
})?;
let indexing = Indexing {
input_offset: 0,
output_offset: 0,
partial_len: None,
active_channels_mask: None,
};
let (_in_used, out_written) = self
.inner
.process_into_buffer(&in_adapter, &mut out_adapter, Some(&indexing))
.map_err(|e| Error::Backend(format!("rubato process_into_buffer failed: {e}")))?;
let n_samples = out_written * self.channels;
out_buf.extend_from_slice(&self.out_scratch[..n_samples]);
produced += out_written as u64;
// Remove consumed input (`FixedAsync::Input` always consumes `chunk_in_frames`).
self.in_accum.drain(..step);
}
Ok(produced)
}
/// On stop, process the less-than-one-chunk remainder in `in_accum` using `partial_len`, append
/// the resulting interleaved samples to `out_buf`, and return the number of output frames.
///
/// This drains the remaining input (just under 20ms at most), leaving `in_accum` empty. It does
/// not flush the resampler's internal filter group delay (a few ms).
pub(super) fn flush_into(&mut self, out_buf: &mut Vec<f32>) -> Result<u64> {
let remaining = self.in_accum.len() / self.channels;
if remaining == 0 {
return Ok(0);
}
// Pad input with silence to one full chunk and pass only the valid length via `partial_len`.
self.in_accum
.resize(self.chunk_in_frames * self.channels, 0.0);
let in_adapter = InterleavedSlice::new(
&self.in_accum[..self.chunk_in_frames * self.channels],
self.channels,
self.chunk_in_frames,
)
.map_err(|e| Error::Backend(format!("rubato interleaved input adapter failed: {e}")))?;
let mut out_adapter = InterleavedSlice::new_mut(
&mut self.out_scratch[..],
self.channels,
self.max_out_frames,
)
.map_err(|e| Error::Backend(format!("rubato interleaved output adapter failed: {e}")))?;
let indexing = Indexing {
input_offset: 0,
output_offset: 0,
partial_len: Some(remaining),
active_channels_mask: None,
};
let (_in_used, out_written) = self
.inner
.process_into_buffer(&in_adapter, &mut out_adapter, Some(&indexing))
.map_err(|e| Error::Backend(format!("rubato flush process_into_buffer failed: {e}")))?;
let n_samples = out_written * self.channels;
out_buf.extend_from_slice(&self.out_scratch[..n_samples]);
self.in_accum.clear();
Ok(out_written as u64)
}
}
impl OutputStage {
/// Create the output stage for the output sample rate and channel count.
///
/// If `out_sample_rate == 48000`, sample-rate conversion is bypassed (only channel conversion
/// is performed). rubato construction failures propagate as [`Error::Backend`].
pub(super) fn new(out_sample_rate: u32, out_channels: usize) -> Result<Self> {
let resampler = if out_sample_rate == SAMPLE_RATE {
None
} else {
// Convert from the 48000-Hz internal canonical form to `out_sample_rate` with `out_channels` channels.
Some(ResamplerState::new(
SAMPLE_RATE,
out_sample_rate,
out_channels,
)?)
};
Ok(Self {
out_channels,
resampler,
ch_scratch: Vec::with_capacity(CHUNK_FRAMES * out_channels),
})
}
/// Process the internal canonical form (48k/stereo interleaved, any length) and append
/// interleaved samples in the output format to `out_buf`. Length must be a multiple of
/// `INNER_CH` (stereo).
pub(super) fn process_inner(
&mut self,
inner_stereo: &[f32],
out_buf: &mut Vec<f32>,
) -> Result<()> {
let frames = inner_stereo.len() / INNER_CH;
if frames == 0 {
return Ok(());
}
// Channel conversion: stereo → out_channels.
self.ch_scratch.clear();
match self.out_channels {
1 => {
// stereo → mono (L/R average).
for f in 0..frames {
let l = inner_stereo[f * 2];
let r = inner_stereo[f * 2 + 1];
self.ch_scratch.push((l + r) * 0.5);
}
}
2 => {
self.ch_scratch
.extend_from_slice(&inner_stereo[..frames * 2]);
}
_ => unreachable!("output channel count validated by constructor"),
}
// Sample-rate conversion: 48000 → out_sample_rate. On passthrough, copy ch_scratch directly to output.
match &mut self.resampler {
None => {
out_buf.extend_from_slice(&self.ch_scratch);
}
Some(rs) => {
rs.in_accum.extend_from_slice(&self.ch_scratch);
rs.drain_into(out_buf)?;
}
}
Ok(())
}
/// On stop, drain any sample-rate resampler remainder into `out_buf` (a passthrough stage has
/// no remainder, so this is a no-op).
pub(super) fn flush_into(&mut self, out_buf: &mut Vec<f32>) -> Result<()> {
if let Some(rs) = self.resampler.as_mut() {
rs.flush_into(out_buf)?;
}
Ok(())
}
}