@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.
- package/LICENSE +25 -0
- package/NOTICE +9 -0
- package/README.md +171 -2
- package/THIRD_PARTY_LICENSES.md +640 -0
- package/dist/engine.d.ts +138 -0
- package/dist/engine.js +275 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +83 -0
- package/dist/state.d.ts +9 -0
- package/dist/state.js +24 -0
- package/dist/workerd.d.ts +8 -0
- package/dist/workerd.js +27 -0
- package/package.json +47 -3
- package/rust/Cargo.lock +137 -0
- package/rust/Cargo.toml +27 -0
- package/rust/src/lib.rs +496 -0
- package/wasm/resampler.wasm +0 -0
package/rust/Cargo.lock
ADDED
|
@@ -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
|
+
]
|
package/rust/Cargo.toml
ADDED
|
@@ -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
|
package/rust/src/lib.rs
ADDED
|
@@ -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
|
+
¶ms,
|
|
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
|