@pipedog/resampler-wasm 0.0.0 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,137 @@
1
+ # This file is automatically @generated by Cargo.
2
+ # It is not intended for manual editing.
3
+ version = 4
4
+
5
+ [[package]]
6
+ name = "audio-codec-algorithms"
7
+ version = "0.8.1"
8
+ source = "registry+https://github.com/rust-lang/crates.io-index"
9
+ checksum = "1254ebf6529f3763c491acfb5ab6e960809e3c75a38584cc664f8c5667fb7107"
10
+
11
+ [[package]]
12
+ name = "audioadapter"
13
+ version = "5.0.0"
14
+ source = "registry+https://github.com/rust-lang/crates.io-index"
15
+ checksum = "d1292ef9edf681b7426ed089004b021a42896492f58bd0c909ad44e5faec9ac4"
16
+
17
+ [[package]]
18
+ name = "audioadapter-buffers"
19
+ version = "5.2.0"
20
+ source = "registry+https://github.com/rust-lang/crates.io-index"
21
+ checksum = "46289a81a3bfa26d0f8b2415f8ca9decde9a66308a150568af5c102381b3c8ce"
22
+ dependencies = [
23
+ "audioadapter",
24
+ "audioadapter-sample",
25
+ "num-traits",
26
+ ]
27
+
28
+ [[package]]
29
+ name = "audioadapter-sample"
30
+ version = "5.2.0"
31
+ source = "registry+https://github.com/rust-lang/crates.io-index"
32
+ checksum = "5258faecf4edbe35ec483bda9b1f7438e6b391891819269c6b5461aeaa0e8d7d"
33
+ dependencies = [
34
+ "audio-codec-algorithms",
35
+ "num-traits",
36
+ ]
37
+
38
+ [[package]]
39
+ name = "autocfg"
40
+ version = "1.5.1"
41
+ source = "registry+https://github.com/rust-lang/crates.io-index"
42
+ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
43
+
44
+ [[package]]
45
+ name = "num-integer"
46
+ version = "0.1.47"
47
+ source = "registry+https://github.com/rust-lang/crates.io-index"
48
+ checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b"
49
+ dependencies = [
50
+ "num-traits",
51
+ ]
52
+
53
+ [[package]]
54
+ name = "num-traits"
55
+ version = "0.2.19"
56
+ source = "registry+https://github.com/rust-lang/crates.io-index"
57
+ checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
58
+ dependencies = [
59
+ "autocfg",
60
+ ]
61
+
62
+ [[package]]
63
+ name = "pipecat-resampler-wasm"
64
+ version = "0.0.0"
65
+ dependencies = [
66
+ "audioadapter-buffers",
67
+ "rubato",
68
+ ]
69
+
70
+ [[package]]
71
+ name = "proc-macro2"
72
+ version = "1.0.107"
73
+ source = "registry+https://github.com/rust-lang/crates.io-index"
74
+ checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
75
+ dependencies = [
76
+ "unicode-ident",
77
+ ]
78
+
79
+ [[package]]
80
+ name = "quote"
81
+ version = "1.0.47"
82
+ source = "registry+https://github.com/rust-lang/crates.io-index"
83
+ checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
84
+ dependencies = [
85
+ "proc-macro2",
86
+ ]
87
+
88
+ [[package]]
89
+ name = "rubato"
90
+ version = "5.0.1"
91
+ source = "registry+https://github.com/rust-lang/crates.io-index"
92
+ checksum = "cc1e951b9f5432ec1422f4dcfa5733fc501265902612edf5ef8bd8ecdd960930"
93
+ dependencies = [
94
+ "audioadapter",
95
+ "audioadapter-buffers",
96
+ "num-integer",
97
+ "num-traits",
98
+ "visibility",
99
+ "windowfunctions",
100
+ ]
101
+
102
+ [[package]]
103
+ name = "syn"
104
+ version = "2.0.119"
105
+ source = "registry+https://github.com/rust-lang/crates.io-index"
106
+ checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
107
+ dependencies = [
108
+ "proc-macro2",
109
+ "quote",
110
+ "unicode-ident",
111
+ ]
112
+
113
+ [[package]]
114
+ name = "unicode-ident"
115
+ version = "1.0.27"
116
+ source = "registry+https://github.com/rust-lang/crates.io-index"
117
+ checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
118
+
119
+ [[package]]
120
+ name = "visibility"
121
+ version = "0.1.1"
122
+ source = "registry+https://github.com/rust-lang/crates.io-index"
123
+ checksum = "d674d135b4a8c1d7e813e2f8d1c9a58308aee4a680323066025e53132218bd91"
124
+ dependencies = [
125
+ "proc-macro2",
126
+ "quote",
127
+ "syn",
128
+ ]
129
+
130
+ [[package]]
131
+ name = "windowfunctions"
132
+ version = "0.1.1"
133
+ source = "registry+https://github.com/rust-lang/crates.io-index"
134
+ checksum = "90628d739333b7c5d2ee0b70210b97b8cddc38440c682c96fd9e2c24c2db5f3a"
135
+ dependencies = [
136
+ "num-traits",
137
+ ]
@@ -0,0 +1,27 @@
1
+ # Copyright (c) 2026, pipedog contributors
2
+ # SPDX-License-Identifier: BSD-2-Clause
3
+
4
+ [package]
5
+ name = "pipecat-resampler-wasm"
6
+ version = "0.0.0"
7
+ edition = "2021"
8
+ license = "BSD-2-Clause"
9
+ publish = false
10
+ description = "rubato sinc resampler behind a tiny C ABI, built for wasm32-unknown-unknown."
11
+
12
+ [lib]
13
+ crate-type = ["cdylib"]
14
+ path = "src/lib.rs"
15
+
16
+ [dependencies]
17
+ # MIT OR Apache-2.0. The FFT resampler (realfft) is not needed: default features off.
18
+ rubato = { version = "=5.0.1", default-features = false }
19
+ audioadapter-buffers = "5.1"
20
+
21
+ [profile.release]
22
+ opt-level = "z"
23
+ lto = true
24
+ codegen-units = 1
25
+ panic = "abort"
26
+ strip = true
27
+ debug = false
@@ -0,0 +1,496 @@
1
+ // Copyright (c) 2026, pipedog contributors
2
+ // SPDX-License-Identifier: BSD-2-Clause
3
+ //
4
+ // A rubato sinc resampler behind a tiny C ABI, for wasm32-unknown-unknown.
5
+ //
6
+ // No wasm-bindgen: the JS side talks to these `extern "C"` functions through
7
+ // `WebAssembly.Instance.exports` and reads/writes PCM directly in linear memory.
8
+ //
9
+ // Model: every resampler is a *stream* (one-shot resampling is "process the
10
+ // whole clip, then flush"). A stream owns its staging buffers:
11
+ //
12
+ // ptr = rs_stream_input(h, n) // room for n int16 samples (interleaved)
13
+ // ... JS copies n samples to ptr ...
14
+ // m = rs_stream_process(h, n) // m int16 samples are now at rs_stream_output(h)
15
+ // m = rs_stream_flush(h) // drain the filter tail, then reset
16
+ //
17
+ // The `_f32` variants do the same with float samples in [-1, 1] (unclipped).
18
+ //
19
+ // Streams accept any number of samples per call. rubato's `Async` resampler
20
+ // processes fixed-size input chunks, so a call is split into pieces of at most
21
+ // MAX_CHUNK frames and the chunk size is set to each piece's length
22
+ // (`Resizable::set_chunk_size` keeps the filter history), which means no
23
+ // input is held back beyond the filter's own delay. A sample count that is
24
+ // not a multiple of the channel count leaves the remainder buffered for the
25
+ // next call.
26
+ //
27
+ // Output is aligned with the input: the filter's start-up delay is trimmed
28
+ // from the front of the stream, and `flush` emits exactly
29
+ // round(total_input_frames * out_rate / in_rate) frames in total, like
30
+ // libsoxr's `ResampleStream` with `last=True`.
31
+
32
+ use audioadapter_buffers::direct::InterleavedSlice;
33
+ use rubato::{
34
+ Async, FixedAsync, Indexing, Resampler, Resizable, SincInterpolationParameters,
35
+ SincInterpolationType, WindowFunction,
36
+ };
37
+
38
+ /// Largest input chunk handed to rubato in one go, in frames.
39
+ const MAX_CHUNK: usize = 1024;
40
+
41
+ /// Quality presets, in the same order as libsoxr's (QQ=0 .. VHQ=4).
42
+ #[derive(Clone, Copy)]
43
+ pub struct Preset {
44
+ pub sinc_len: usize,
45
+ pub oversampling: usize,
46
+ pub window: WindowFunction,
47
+ pub interpolation: SincInterpolationType,
48
+ }
49
+
50
+ /// The anti-aliasing cutoff is left automatic (`f_cutoff: None`): rubato's
51
+ /// `calculate_cutoff` picks the highest cutoff that keeps aliasing below the
52
+ /// window's sidelobe level for that filter length, so it rises with sinc_len.
53
+ pub fn preset(quality: u32) -> Option<Preset> {
54
+ use SincInterpolationType::*;
55
+ use WindowFunction::*;
56
+ let p = |sinc_len, oversampling, window, interpolation| Preset {
57
+ sinc_len,
58
+ oversampling,
59
+ window,
60
+ interpolation,
61
+ };
62
+ Some(match quality {
63
+ 0 => p(16, 32, Hann2, Linear), // QQ
64
+ 1 => p(32, 64, Blackman2, Linear), // LQ
65
+ 2 => p(64, 128, Blackman2, Quadratic), // MQ
66
+ 3 => p(128, 256, BlackmanHarris2, Cubic), // HQ
67
+ 4 => p(256, 256, BlackmanHarris2, Cubic), // VHQ
68
+ _ => return None,
69
+ })
70
+ }
71
+
72
+ pub struct Stream {
73
+ resampler: Async<f32>,
74
+ channels: usize,
75
+ ratio: f64,
76
+ cutoff: f32,
77
+ /// Output frames to drop at the start of every stream (filter delay).
78
+ delay: usize,
79
+ /// Zero frames fed before the first input so that `delay` is a whole
80
+ /// number of output frames (see `alignment`).
81
+ prime: usize,
82
+ /// Whether the priming zeros still have to be fed.
83
+ needs_prime: bool,
84
+ /// Output frames still to drop at the start of this stream.
85
+ delay_left: usize,
86
+ /// Input frames consumed since the stream started (or was last reset).
87
+ total_in: u64,
88
+ /// Output frames emitted since the stream started (or was last reset).
89
+ total_out: u64,
90
+ /// Samples (< channels) left over from the previous call.
91
+ carry: Vec<f32>,
92
+ /// Staging areas JS writes input into.
93
+ input_i16: Vec<i16>,
94
+ input_f32: Vec<f32>,
95
+ /// Interleaved float input (carry + input).
96
+ in_f: Vec<f32>,
97
+ /// One rubato output chunk.
98
+ out_f: Vec<f32>,
99
+ /// The result of the last process/flush call.
100
+ res: Vec<f32>,
101
+ output_i16: Vec<i16>,
102
+ }
103
+
104
+ impl Stream {
105
+ pub fn new(in_rate: u32, out_rate: u32, channels: u32, quality: u32) -> Option<Stream> {
106
+ if in_rate == 0 || out_rate == 0 || channels == 0 || channels > 8 {
107
+ return None;
108
+ }
109
+ let p = preset(quality)?;
110
+ let ratio = out_rate as f64 / in_rate as f64;
111
+ let params = SincInterpolationParameters::new(p.sinc_len, p.window)
112
+ .oversampling_factor(p.oversampling)
113
+ .interpolation(p.interpolation);
114
+ let resampler = Async::<f32>::new_sinc(
115
+ ratio,
116
+ 1.0,
117
+ &params,
118
+ MAX_CHUNK,
119
+ channels as usize,
120
+ FixedAsync::Input,
121
+ )
122
+ .ok()?;
123
+ let cutoff = resampler.cutoff().unwrap_or(0.0);
124
+ let out_cap = resampler.output_frames_max() * channels as usize;
125
+ let (prime, delay) = alignment(in_rate, out_rate, p.sinc_len);
126
+ Some(Stream {
127
+ resampler,
128
+ channels: channels as usize,
129
+ ratio,
130
+ cutoff,
131
+ delay,
132
+ prime,
133
+ needs_prime: true,
134
+ delay_left: delay,
135
+ total_in: 0,
136
+ total_out: 0,
137
+ carry: Vec::new(),
138
+ input_i16: Vec::new(),
139
+ input_f32: Vec::new(),
140
+ in_f: Vec::new(),
141
+ out_f: vec![0.0; out_cap],
142
+ res: Vec::new(),
143
+ output_i16: Vec::new(),
144
+ })
145
+ }
146
+
147
+ pub fn reset(&mut self) {
148
+ self.resampler.reset();
149
+ self.needs_prime = true;
150
+ self.delay_left = self.delay;
151
+ self.total_in = 0;
152
+ self.total_out = 0;
153
+ self.carry.clear();
154
+ }
155
+
156
+ /// Run one rubato chunk of `frames` input frames starting at frame
157
+ /// `offset` of `in_f` (`silence` feeds a full chunk of zeros instead), and
158
+ /// append the delay-trimmed result to `res`.
159
+ fn run_chunk(&mut self, offset: usize, frames: usize, silence: bool) -> bool {
160
+ let ch = self.channels;
161
+ let chunk = if silence { MAX_CHUNK } else { frames };
162
+ if self.resampler.set_chunk_size(chunk).is_err() {
163
+ return false;
164
+ }
165
+ let (src, src_frames, indexing) = if silence {
166
+ (&[][..], 0, Indexing::new().partial_len(0))
167
+ } else {
168
+ (
169
+ &self.in_f[..],
170
+ self.in_f.len() / ch,
171
+ Indexing::new().input_offset(offset),
172
+ )
173
+ };
174
+ let Ok(input) = InterleavedSlice::new(src, ch, src_frames) else {
175
+ return false;
176
+ };
177
+ let out_frames = self.out_f.len() / ch;
178
+ let Ok(mut output) = InterleavedSlice::new_mut(&mut self.out_f, ch, out_frames) else {
179
+ return false;
180
+ };
181
+ let Ok((_, produced)) =
182
+ self.resampler
183
+ .process_into_buffer(&input, &mut output, Some(&indexing))
184
+ else {
185
+ return false;
186
+ };
187
+ let skip = self.delay_left.min(produced);
188
+ self.delay_left -= skip;
189
+ self.total_out += (produced - skip) as u64;
190
+ self.res
191
+ .extend_from_slice(&self.out_f[skip * ch..produced * ch]);
192
+ true
193
+ }
194
+
195
+ /// Resample `samples` (interleaved, float). The result is left in `res`.
196
+ fn process(&mut self, samples: impl Iterator<Item = f32>) -> bool {
197
+ let ch = self.channels;
198
+ self.res.clear();
199
+ self.in_f.clear();
200
+ if self.needs_prime {
201
+ self.needs_prime = false;
202
+ self.in_f.resize(self.prime * ch, 0.0);
203
+ }
204
+ let primed = self.in_f.len() / ch;
205
+ self.in_f.extend_from_slice(&self.carry);
206
+ self.in_f.extend(samples);
207
+ let frames = self.in_f.len() / ch;
208
+ self.carry.clear();
209
+ self.carry.extend_from_slice(&self.in_f[frames * ch..]);
210
+ self.in_f.truncate(frames * ch);
211
+ self.total_in += (frames - primed) as u64;
212
+
213
+ let mut offset = 0;
214
+ while offset < frames {
215
+ let piece = (frames - offset).min(MAX_CHUNK);
216
+ if !self.run_chunk(offset, piece, false) {
217
+ return false;
218
+ }
219
+ offset += piece;
220
+ }
221
+ true
222
+ }
223
+
224
+ /// Emit the audio held in the filter, then reset. The result is left in `res`.
225
+ fn flush(&mut self) -> bool {
226
+ let ch = self.channels;
227
+ self.res.clear();
228
+ let expected = (self.total_in as f64 * self.ratio).round() as u64;
229
+ // A few silent chunks always cover the filter delay; bound the loop anyway.
230
+ let mut guard = 64;
231
+ while self.total_out < expected && guard > 0 {
232
+ if !self.run_chunk(0, 0, true) {
233
+ return false;
234
+ }
235
+ guard -= 1;
236
+ }
237
+ if self.total_out > expected {
238
+ let extra = (self.total_out - expected) as usize;
239
+ let keep = self.res.len().saturating_sub(extra * ch);
240
+ self.res.truncate(keep);
241
+ }
242
+ self.reset();
243
+ true
244
+ }
245
+
246
+ fn result_i16(&mut self, ok: bool) -> i32 {
247
+ self.output_i16.clear();
248
+ if !ok {
249
+ return -1;
250
+ }
251
+ self.output_i16.extend(self.res.iter().map(|&v| to_i16(v)));
252
+ self.output_i16.len() as i32
253
+ }
254
+
255
+ fn result_f32(&self, ok: bool) -> i32 {
256
+ if ok {
257
+ self.res.len() as i32
258
+ } else {
259
+ -1
260
+ }
261
+ }
262
+ }
263
+
264
+ /// Choose how many zero frames to prime a stream with, and how many output
265
+ /// frames to trim, so the output is sample-aligned with the input.
266
+ ///
267
+ /// rubato's first output frame sits at input position `1/ratio - sinc_len/2`,
268
+ /// i.e. the stream is delayed by `D = sinc_len*ratio/2 - 1` output frames.
269
+ /// `D` is fractional for many ratios (e.g. 48k->16k), and a fractional delay
270
+ /// cannot be trimmed. Priming with `k` zero input frames adds `k*ratio`
271
+ /// output frames of delay; with ratio = p/q in lowest terms some k < 2q makes
272
+ /// the total a whole number. Returns `(k, round(total delay))`.
273
+ fn alignment(in_rate: u32, out_rate: u32, sinc_len: usize) -> (usize, usize) {
274
+ let g = gcd(in_rate as u64, out_rate as u64);
275
+ let (p, q) = ((out_rate as u64 / g) as i128, (in_rate as u64 / g) as i128);
276
+ let l = sinc_len.div_ceil(8) as i128 * 8;
277
+ // total(k) = (l*p + 2*k*p - 2*q) / (2*q) output frames.
278
+ let den = 2 * q;
279
+ let mut best = (0i128, i128::MAX);
280
+ for k in 0..den.min(1 << 16) {
281
+ let num = l * p + 2 * k * p - 2 * q;
282
+ let rem = num.rem_euclid(den).min(den - num.rem_euclid(den));
283
+ if rem < best.1 {
284
+ best = (k, rem);
285
+ if rem == 0 {
286
+ break;
287
+ }
288
+ }
289
+ }
290
+ let k = best.0;
291
+ let num = l * p + 2 * k * p - 2 * q;
292
+ let delay = ((num as f64) / (den as f64)).round().max(0.0) as usize;
293
+ (k as usize, delay)
294
+ }
295
+
296
+ fn gcd(mut a: u64, mut b: u64) -> u64 {
297
+ while b != 0 {
298
+ (a, b) = (b, a % b);
299
+ }
300
+ a.max(1)
301
+ }
302
+
303
+ #[inline]
304
+ fn to_i16(v: f32) -> i16 {
305
+ let s = (v * 32768.0).round();
306
+ if s >= 32767.0 {
307
+ 32767
308
+ } else if s <= -32768.0 {
309
+ -32768
310
+ } else {
311
+ s as i16
312
+ }
313
+ }
314
+
315
+ // ---------------------------------------------------------------------------
316
+ // C ABI
317
+ // ---------------------------------------------------------------------------
318
+
319
+ /// Allocate `len` bytes (8-byte aligned) in linear memory.
320
+ #[no_mangle]
321
+ pub extern "C" fn rs_alloc(len: usize) -> *mut u8 {
322
+ let words = len.div_ceil(8).max(1);
323
+ let mut v: Vec<u64> = Vec::with_capacity(words);
324
+ let ptr = v.as_mut_ptr() as *mut u8;
325
+ core::mem::forget(v);
326
+ ptr
327
+ }
328
+
329
+ /// Free a block returned by `rs_alloc(len)`.
330
+ ///
331
+ /// # Safety
332
+ /// `ptr` must come from `rs_alloc` with the same `len`.
333
+ #[no_mangle]
334
+ pub unsafe extern "C" fn rs_free(ptr: *mut u8, len: usize) {
335
+ if ptr.is_null() {
336
+ return;
337
+ }
338
+ let words = len.div_ceil(8).max(1);
339
+ drop(Vec::from_raw_parts(ptr as *mut u64, 0, words));
340
+ }
341
+
342
+ /// Create a stream. `quality`: 0=QQ, 1=LQ, 2=MQ, 3=HQ, 4=VHQ. Returns 0 on
343
+ /// invalid parameters (zero rate, channels outside 1..=8, unknown quality).
344
+ #[no_mangle]
345
+ pub extern "C" fn rs_stream_new(
346
+ in_rate: u32,
347
+ out_rate: u32,
348
+ channels: u32,
349
+ quality: u32,
350
+ ) -> *mut Stream {
351
+ match Stream::new(in_rate, out_rate, channels, quality) {
352
+ Some(s) => Box::into_raw(Box::new(s)),
353
+ None => core::ptr::null_mut(),
354
+ }
355
+ }
356
+
357
+ /// # Safety
358
+ /// `h` must be a live handle from `rs_stream_new`; it is invalid afterwards.
359
+ #[no_mangle]
360
+ pub unsafe extern "C" fn rs_stream_free(h: *mut Stream) {
361
+ if !h.is_null() {
362
+ drop(Box::from_raw(h));
363
+ }
364
+ }
365
+
366
+ /// Make room for `n` int16 input samples and return where to write them.
367
+ ///
368
+ /// # Safety
369
+ /// `h` must be a live handle from `rs_stream_new`.
370
+ #[no_mangle]
371
+ pub unsafe extern "C" fn rs_stream_input(h: *mut Stream, n: usize) -> *mut i16 {
372
+ let s = &mut *h;
373
+ s.input_i16.resize(n, 0);
374
+ s.input_i16.as_mut_ptr()
375
+ }
376
+
377
+ /// Resample the `n` int16 samples written at `rs_stream_input`. Returns the
378
+ /// number of int16 samples now at `rs_stream_output`, or -1 on error.
379
+ ///
380
+ /// # Safety
381
+ /// `h` must be a live handle from `rs_stream_new`.
382
+ #[no_mangle]
383
+ pub unsafe extern "C" fn rs_stream_process(h: *mut Stream, n: usize) -> i32 {
384
+ let s = &mut *h;
385
+ let input = core::mem::take(&mut s.input_i16);
386
+ let n = n.min(input.len());
387
+ let ok = s.process(input[..n].iter().map(|&v| v as f32 / 32768.0));
388
+ s.input_i16 = input;
389
+ s.result_i16(ok)
390
+ }
391
+
392
+ /// Drain the filter tail and reset. Returns the number of int16 samples now
393
+ /// at `rs_stream_output`, or -1 on error.
394
+ ///
395
+ /// # Safety
396
+ /// `h` must be a live handle from `rs_stream_new`.
397
+ #[no_mangle]
398
+ pub unsafe extern "C" fn rs_stream_flush(h: *mut Stream) -> i32 {
399
+ let s = &mut *h;
400
+ let ok = s.flush();
401
+ s.result_i16(ok)
402
+ }
403
+
404
+ /// Pointer to the int16 output of the last `rs_stream_process`/`rs_stream_flush`.
405
+ ///
406
+ /// # Safety
407
+ /// `h` must be a live handle from `rs_stream_new`.
408
+ #[no_mangle]
409
+ pub unsafe extern "C" fn rs_stream_output(h: *mut Stream) -> *const i16 {
410
+ (*h).output_i16.as_ptr()
411
+ }
412
+
413
+ /// Make room for `n` float input samples and return where to write them.
414
+ ///
415
+ /// # Safety
416
+ /// `h` must be a live handle from `rs_stream_new`.
417
+ #[no_mangle]
418
+ pub unsafe extern "C" fn rs_stream_input_f32(h: *mut Stream, n: usize) -> *mut f32 {
419
+ let s = &mut *h;
420
+ s.input_f32.resize(n, 0.0);
421
+ s.input_f32.as_mut_ptr()
422
+ }
423
+
424
+ /// Resample the `n` float samples written at `rs_stream_input_f32`. Returns
425
+ /// the number of float samples now at `rs_stream_output_f32`, or -1 on error.
426
+ ///
427
+ /// # Safety
428
+ /// `h` must be a live handle from `rs_stream_new`.
429
+ #[no_mangle]
430
+ pub unsafe extern "C" fn rs_stream_process_f32(h: *mut Stream, n: usize) -> i32 {
431
+ let s = &mut *h;
432
+ let input = core::mem::take(&mut s.input_f32);
433
+ let n = n.min(input.len());
434
+ let ok = s.process(input[..n].iter().copied());
435
+ s.input_f32 = input;
436
+ s.result_f32(ok)
437
+ }
438
+
439
+ /// Float variant of `rs_stream_flush`; output at `rs_stream_output_f32`.
440
+ ///
441
+ /// # Safety
442
+ /// `h` must be a live handle from `rs_stream_new`.
443
+ #[no_mangle]
444
+ pub unsafe extern "C" fn rs_stream_flush_f32(h: *mut Stream) -> i32 {
445
+ let s = &mut *h;
446
+ let ok = s.flush();
447
+ s.result_f32(ok)
448
+ }
449
+
450
+ /// Pointer to the float output of the last `_f32` process/flush call.
451
+ ///
452
+ /// # Safety
453
+ /// `h` must be a live handle from `rs_stream_new`.
454
+ #[no_mangle]
455
+ pub unsafe extern "C" fn rs_stream_output_f32(h: *mut Stream) -> *const f32 {
456
+ (*h).res.as_ptr()
457
+ }
458
+
459
+ /// Discard all state (filter history, pending samples, counters).
460
+ ///
461
+ /// # Safety
462
+ /// `h` must be a live handle from `rs_stream_new`.
463
+ #[no_mangle]
464
+ pub unsafe extern "C" fn rs_stream_reset(h: *mut Stream) {
465
+ (*h).reset()
466
+ }
467
+
468
+ /// Filter delay in output frames (trimmed from the start of every stream).
469
+ ///
470
+ /// # Safety
471
+ /// `h` must be a live handle from `rs_stream_new`.
472
+ #[no_mangle]
473
+ pub unsafe extern "C" fn rs_stream_delay(h: *mut Stream) -> u32 {
474
+ (*h).delay as u32
475
+ }
476
+
477
+ /// Relative anti-aliasing cutoff (1.0 = input Nyquist; scaled down when downsampling).
478
+ ///
479
+ /// # Safety
480
+ /// `h` must be a live handle from `rs_stream_new`.
481
+ #[no_mangle]
482
+ pub unsafe extern "C" fn rs_stream_cutoff(h: *mut Stream) -> f32 {
483
+ (*h).cutoff
484
+ }
485
+
486
+ /// Sinc filter length of a quality preset (0 if unknown).
487
+ #[no_mangle]
488
+ pub extern "C" fn rs_quality_sinc_len(quality: u32) -> u32 {
489
+ preset(quality).map(|p| p.sinc_len as u32).unwrap_or(0)
490
+ }
491
+
492
+ /// Oversampling factor of a quality preset (0 if unknown).
493
+ #[no_mangle]
494
+ pub extern "C" fn rs_quality_oversampling(quality: u32) -> u32 {
495
+ preset(quality).map(|p| p.oversampling as u32).unwrap_or(0)
496
+ }
Binary file