Skip to content

Commit 9697241

Browse files
authored
24k sample-rate in macOS to minimize resampling (#1741)
1 parent 0ff6873 commit 9697241

File tree

8 files changed

+191
-51
lines changed

8 files changed

+191
-51
lines changed

crates/audio-utils/src/resampler/dynamic_new.rs

Lines changed: 133 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,107 @@ pub trait ResampleExtDynamicNew: AsyncSource + Sized + Unpin {
1818

1919
impl<T> ResampleExtDynamicNew for T where T: AsyncSource + Sized + Unpin {}
2020

21+
enum Backend {
22+
Passthrough(Vec<f32>),
23+
Resampler(RubatoChunkResampler<FastFixedIn<f32>, 1>),
24+
}
25+
26+
impl Backend {
27+
fn passthrough(capacity: usize) -> Self {
28+
Self::Passthrough(Vec::with_capacity(capacity))
29+
}
30+
31+
fn ensure_passthrough(&mut self, capacity: usize) {
32+
match self {
33+
Self::Passthrough(buffer) => buffer.clear(),
34+
Self::Resampler(_) => *self = Self::passthrough(capacity),
35+
}
36+
}
37+
38+
fn ensure_resampler(
39+
&mut self,
40+
resampler: FastFixedIn<f32>,
41+
output_chunk_size: usize,
42+
input_block_size: usize,
43+
) {
44+
match self {
45+
Self::Passthrough(_) => {
46+
*self = Self::Resampler(RubatoChunkResampler::new(
47+
resampler,
48+
output_chunk_size,
49+
input_block_size,
50+
));
51+
}
52+
Self::Resampler(driver) => {
53+
driver.rebind_resampler(resampler, output_chunk_size, input_block_size)
54+
}
55+
}
56+
}
57+
58+
fn push_sample(&mut self, sample: f32) {
59+
match self {
60+
Self::Passthrough(buffer) => buffer.push(sample),
61+
Self::Resampler(driver) => driver.push_sample(sample),
62+
}
63+
}
64+
65+
fn try_yield_chunk(&mut self, chunk_size: usize, allow_partial: bool) -> Option<Vec<f32>> {
66+
match self {
67+
Self::Passthrough(buffer) => {
68+
if buffer.len() >= chunk_size {
69+
Some(buffer.drain(..chunk_size).collect())
70+
} else if allow_partial && !buffer.is_empty() {
71+
Some(buffer.drain(..).collect())
72+
} else {
73+
None
74+
}
75+
}
76+
Self::Resampler(driver) => {
77+
if driver.has_full_chunk() {
78+
driver.take_full_chunk()
79+
} else if allow_partial && !driver.output_is_empty() {
80+
driver.take_all_output()
81+
} else {
82+
None
83+
}
84+
}
85+
}
86+
}
87+
88+
fn process_all_ready_blocks(&mut self) -> Result<bool, crate::Error> {
89+
match self {
90+
Self::Passthrough(_) => Ok(false),
91+
Self::Resampler(driver) => driver.process_all_ready_blocks(),
92+
}
93+
}
94+
95+
fn drain_for_rate_change(&mut self) -> Result<bool, crate::Error> {
96+
match self {
97+
Self::Passthrough(buffer) => Ok(buffer.is_empty()),
98+
Self::Resampler(driver) => {
99+
driver.process_all_ready_blocks()?;
100+
if driver.has_input() {
101+
driver.process_partial_block(true)?;
102+
}
103+
Ok(driver.output_is_empty())
104+
}
105+
}
106+
}
107+
108+
fn drain_at_eos(&mut self) -> Result<(), crate::Error> {
109+
match self {
110+
Self::Passthrough(_) => Ok(()),
111+
Self::Resampler(driver) => {
112+
driver.process_all_ready_blocks()?;
113+
if driver.has_input() {
114+
driver.process_partial_block(true)?;
115+
}
116+
Ok(())
117+
}
118+
}
119+
}
120+
}
121+
21122
pub struct ResamplerDynamicNew<S>
22123
where
23124
S: AsyncSource + Unpin,
@@ -26,7 +127,7 @@ where
26127
target_rate: u32,
27128
output_chunk_size: usize,
28129
input_block_size: usize,
29-
driver: RubatoChunkResampler<FastFixedIn<f32>, 1>,
130+
backend: Backend,
30131
last_source_rate: u32,
31132
draining: bool,
32133
}
@@ -42,53 +143,51 @@ where
42143
) -> Result<Self, crate::Error> {
43144
let source_rate = source.sample_rate();
44145
let input_block_size = output_chunk_size;
45-
let ratio = target_rate as f64 / source_rate as f64;
46-
let resampler = Self::create_resampler(ratio, input_block_size)?;
47-
let driver = RubatoChunkResampler::new(resampler, output_chunk_size, input_block_size);
146+
let backend = if source_rate == target_rate {
147+
Backend::passthrough(output_chunk_size)
148+
} else {
149+
let ratio = target_rate as f64 / source_rate as f64;
150+
Backend::Resampler(RubatoChunkResampler::new(
151+
Self::create_resampler(ratio, input_block_size)?,
152+
output_chunk_size,
153+
input_block_size,
154+
))
155+
};
48156
Ok(Self {
49157
source,
50158
target_rate,
51159
output_chunk_size,
52160
input_block_size,
53-
driver,
161+
backend,
54162
last_source_rate: source_rate,
55163
draining: false,
56164
})
57165
}
58166

59-
fn rebuild_resampler(&mut self, new_rate: u32) -> Result<(), crate::Error> {
60-
let ratio = self.target_rate as f64 / new_rate as f64;
61-
let resampler = Self::create_resampler(ratio, self.input_block_size)?;
62-
self.driver
63-
.rebind_resampler(resampler, self.output_chunk_size, self.input_block_size);
167+
fn rebuild_backend(&mut self, new_rate: u32) -> Result<(), crate::Error> {
168+
if new_rate == self.target_rate {
169+
self.backend.ensure_passthrough(self.output_chunk_size);
170+
} else {
171+
let ratio = self.target_rate as f64 / new_rate as f64;
172+
let resampler = Self::create_resampler(ratio, self.input_block_size)?;
173+
self.backend
174+
.ensure_resampler(resampler, self.output_chunk_size, self.input_block_size);
175+
}
64176
self.last_source_rate = new_rate;
65177
Ok(())
66178
}
67179

68-
fn try_yield_chunk(&mut self) -> Option<Vec<f32>> {
69-
if self.driver.has_full_chunk() {
70-
self.driver.take_full_chunk()
71-
} else if self.draining && !self.driver.output_is_empty() {
72-
self.driver.take_all_output()
73-
} else {
74-
None
75-
}
180+
fn try_yield_chunk(&mut self, allow_partial: bool) -> Option<Vec<f32>> {
181+
self.backend
182+
.try_yield_chunk(self.output_chunk_size, allow_partial)
76183
}
77184

78185
fn drain_for_rate_change(&mut self) -> Result<bool, crate::Error> {
79-
self.driver.process_all_ready_blocks()?;
80-
if self.driver.has_input() {
81-
self.driver.process_partial_block(true)?;
82-
}
83-
Ok(self.driver.output_is_empty())
186+
self.backend.drain_for_rate_change()
84187
}
85188

86189
fn drain_at_eos(&mut self) -> Result<(), crate::Error> {
87-
self.driver.process_all_ready_blocks()?;
88-
if self.driver.has_input() {
89-
self.driver.process_partial_block(true)?;
90-
}
91-
Ok(())
190+
self.backend.drain_at_eos()
92191
}
93192

94193
fn create_resampler(
@@ -116,7 +215,7 @@ where
116215
let me = Pin::into_inner(self);
117216

118217
loop {
119-
if let Some(chunk) = me.try_yield_chunk() {
218+
if let Some(chunk) = me.try_yield_chunk(me.draining) {
120219
return Poll::Ready(Some(Ok(chunk)));
121220
}
122221

@@ -128,29 +227,22 @@ where
128227
if current_rate != me.last_source_rate {
129228
match me.drain_for_rate_change() {
130229
Ok(true) => {
131-
if let Err(err) = me.rebuild_resampler(current_rate) {
230+
if let Err(err) = me.rebuild_backend(current_rate) {
132231
return Poll::Ready(Some(Err(err)));
133232
}
134233
continue;
135234
}
136235
Ok(false) => {
137-
if me.driver.has_full_chunk() {
138-
if let Some(chunk) = me.driver.take_full_chunk() {
139-
return Poll::Ready(Some(Ok(chunk)));
140-
}
141-
}
142-
if !me.driver.output_is_empty() {
143-
if let Some(chunk) = me.driver.take_all_output() {
144-
return Poll::Ready(Some(Ok(chunk)));
145-
}
236+
if let Some(chunk) = me.try_yield_chunk(true) {
237+
return Poll::Ready(Some(Ok(chunk)));
146238
}
147239
continue;
148240
}
149241
Err(err) => return Poll::Ready(Some(Err(err))),
150242
}
151243
}
152244

153-
match me.driver.process_all_ready_blocks() {
245+
match me.backend.process_all_ready_blocks() {
154246
Ok(true) => continue,
155247
Ok(false) => {}
156248
Err(err) => return Poll::Ready(Some(Err(err))),
@@ -164,7 +256,7 @@ where
164256

165257
match sample_poll {
166258
Poll::Ready(Some(sample)) => {
167-
me.driver.push_sample(sample);
259+
me.backend.push_sample(sample);
168260
}
169261
Poll::Ready(None) => {
170262
if let Err(err) = me.drain_at_eos() {

crates/audio-utils/src/resampler/dynamic_old.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,13 @@ pub struct ResamplerDynamicOld<S: AsyncSource> {
1616
interp: dasp::interpolate::linear::Linear<f32>,
1717
last_sample: f32,
1818
seeded: bool,
19+
bypass: bool,
1920
}
2021

2122
impl<S: AsyncSource> ResamplerDynamicOld<S> {
2223
pub fn new(source: S, target_sample_rate: u32) -> Self {
2324
let initial_rate = source.sample_rate();
25+
let bypass = initial_rate == target_sample_rate;
2426
Self {
2527
source,
2628
target_sample_rate,
@@ -30,6 +32,7 @@ impl<S: AsyncSource> ResamplerDynamicOld<S> {
3032
interp: dasp::interpolate::linear::Linear::new(0.0, 0.0),
3133
last_sample: 0.0,
3234
seeded: false,
35+
bypass,
3336
}
3437
}
3538

@@ -41,6 +44,7 @@ impl<S: AsyncSource> ResamplerDynamicOld<S> {
4144
}
4245

4346
self.last_source_rate = new_rate;
47+
self.bypass = new_rate == self.target_sample_rate;
4448
self.ratio = new_rate as f64 / self.target_sample_rate as f64;
4549
self.phase = 0.0;
4650
self.interp = dasp::interpolate::linear::Linear::new(self.last_sample, self.last_sample);
@@ -58,6 +62,10 @@ impl<S: AsyncSource + Unpin> Stream for ResamplerDynamicOld<S> {
5862
let inner = me.source.as_stream();
5963
pin_mut!(inner);
6064

65+
if me.bypass {
66+
return inner.as_mut().poll_next(cx);
67+
}
68+
6169
if !me.seeded {
6270
match inner.as_mut().poll_next(cx) {
6371
Poll::Ready(Some(frame)) => {

crates/audio-utils/src/resampler/mod.rs

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ mod tests {
133133
async fn test_kalosm_builtin_resampler() {
134134
let source = create_test_source();
135135
let resampled = source.resample(16000);
136-
assert_eq!(resampled.collect::<Vec<_>>().await.len(), 9896247);
136+
assert_eq!(resampled.collect::<Vec<_>>().await.len(), 9906153);
137137
}
138138

139139
#[tokio::test]
@@ -143,7 +143,7 @@ mod tests {
143143
.collect::<Vec<_>>()
144144
.await;
145145

146-
assert_eq!(samples.len(), 2791777);
146+
assert_eq!(samples.len(), 2791776);
147147
write_wav!("dynamic_old_resampler.wav", 16000, samples.iter().copied());
148148
}
149149

@@ -165,6 +165,43 @@ mod tests {
165165
);
166166
}
167167

168+
#[tokio::test]
169+
async fn test_dynamic_new_resampler_passthrough() {
170+
let (original_sample_rate, original_samples) = {
171+
let mut static_source = DynamicRateSource::new(vec![get_samples_with_rate(
172+
hypr_data::english_1::AUDIO_PART2_16000HZ_PATH,
173+
)]);
174+
175+
let original_sample_rate = static_source.sample_rate();
176+
let original_samples = static_source.as_stream().collect::<Vec<_>>().await;
177+
178+
(original_sample_rate, original_samples)
179+
};
180+
181+
let (resampler_sample_rate, resampled_samples) = {
182+
let static_source = DynamicRateSource::new(vec![get_samples_with_rate(
183+
hypr_data::english_1::AUDIO_PART2_16000HZ_PATH,
184+
)]);
185+
186+
let resampler_sample_rate = static_source.sample_rate();
187+
let chunk_size = 1920;
188+
let resampler =
189+
ResamplerDynamicNew::new(static_source, resampler_sample_rate, chunk_size).unwrap();
190+
191+
let chunks: Vec<_> = resampler.collect::<Vec<_>>().await;
192+
let resampled_samples: Vec<f32> = chunks
193+
.into_iter()
194+
.filter_map(|r| r.ok())
195+
.flatten()
196+
.collect();
197+
198+
(resampler_sample_rate, resampled_samples)
199+
};
200+
201+
assert_eq!(resampler_sample_rate, original_sample_rate);
202+
assert_eq!(resampled_samples, original_samples);
203+
}
204+
168205
#[tokio::test]
169206
async fn test_static_new_resampler() {
170207
let static_source = DynamicRateSource::new(vec![get_samples_with_rate(

plugins/listener/src/actors/controller.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
use std::time::{Instant, SystemTime};
22

3-
use tauri::Manager;
43
use tauri_specta::Event;
54
use tokio_util::sync::CancellationToken;
65

plugins/listener/src/actors/listener.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ async fn spawn_rx_task(
209209
.params(owhisper_interface::ListenParams {
210210
model: Some(args.model.clone()),
211211
languages: args.languages.clone(),
212-
sample_rate: 16000,
212+
sample_rate: super::SAMPLE_RATE,
213213
redemption_time_ms: Some(if args.onboarding { 60 } else { 400 }),
214214
keywords: args.keywords.clone(),
215215
..Default::default()

plugins/listener/src/actors/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ pub use listener::*;
1010
pub use recorder::*;
1111
pub use source::*;
1212

13+
#[cfg(target_os = "macos")]
14+
pub const SAMPLE_RATE: u32 = 24 * 1000;
15+
#[cfg(not(target_os = "macos"))]
16+
pub const SAMPLE_RATE: u32 = 16 * 1000;
17+
1318
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1419
pub enum ChannelMode {
1520
Single,

plugins/listener/src/actors/recorder.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ impl Actor for RecorderActor {
5959

6060
let spec = hound::WavSpec {
6161
channels: 1,
62-
sample_rate: 16000,
62+
sample_rate: super::SAMPLE_RATE,
6363
bits_per_sample: 32,
6464
sample_format: hound::SampleFormat::Float,
6565
};

0 commit comments

Comments
 (0)