@convis/audio-native 0.0.6 → 0.0.8-rc1
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/dist/index.d.ts +6 -0
- package/package.json +7 -7
- package/src/lib.rs +102 -15
package/dist/index.d.ts
CHANGED
|
@@ -11,6 +11,12 @@ export declare class AudioHardware {
|
|
|
11
11
|
openOutputStream(cfg: StreamConfig): void
|
|
12
12
|
openInputStream(cfg: StreamConfig): void
|
|
13
13
|
write(pcm: Float32Array): void
|
|
14
|
+
/**
|
|
15
|
+
* Sets the output channels the audio is played on: "mono" (all channels), "left" or
|
|
16
|
+
* "right". It applies to the current and future output streams, and is ignored by mono
|
|
17
|
+
* devices.
|
|
18
|
+
*/
|
|
19
|
+
setOutputChannel(channel: string): void
|
|
14
20
|
setInputCallback(cb: ((err: Error | null, arg: Float32Array) => any)): void
|
|
15
21
|
}
|
|
16
22
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@convis/audio-native",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8-rc1",
|
|
4
4
|
"description": "Native Node (N-API) module for @convis/audio",
|
|
5
5
|
"homepage": "https://github.com/k2d222/convis-audio",
|
|
6
6
|
"bugs": "https://github.com/k2d222/convis-audio/issues",
|
|
@@ -22,12 +22,12 @@
|
|
|
22
22
|
"@types/node": "^25.6.2"
|
|
23
23
|
},
|
|
24
24
|
"optionalDependencies": {
|
|
25
|
-
"@convis/audio-aarch64-
|
|
26
|
-
"@convis/audio-aarch64-unknown-linux-gnu": "0.0.
|
|
27
|
-
"@convis/audio-
|
|
28
|
-
"@convis/audio-x86_64-apple-darwin": "0.0.
|
|
29
|
-
"@convis/audio-
|
|
30
|
-
"@convis/audio-x86_64-pc-windows-msvc": "0.0.
|
|
25
|
+
"@convis/audio-aarch64-pc-windows-msvc": "0.0.8-rc1",
|
|
26
|
+
"@convis/audio-aarch64-unknown-linux-gnu": "0.0.8-rc1",
|
|
27
|
+
"@convis/audio-x86_64-unknown-linux-gnu": "0.0.8-rc1",
|
|
28
|
+
"@convis/audio-x86_64-apple-darwin": "0.0.8-rc1",
|
|
29
|
+
"@convis/audio-aarch64-apple-darwin": "0.0.8-rc1",
|
|
30
|
+
"@convis/audio-x86_64-pc-windows-msvc": "0.0.8-rc1"
|
|
31
31
|
},
|
|
32
32
|
"napi": {
|
|
33
33
|
"npmClient": "pnpm",
|
package/src/lib.rs
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
use std::sync::{
|
|
2
|
-
atomic::{AtomicUsize, Ordering},
|
|
2
|
+
atomic::{AtomicU8, AtomicUsize, Ordering},
|
|
3
3
|
Arc, Mutex,
|
|
4
4
|
};
|
|
5
5
|
|
|
6
6
|
use anyhow::Context;
|
|
7
7
|
use cpal::{
|
|
8
8
|
traits::{DeviceTrait, HostTrait, StreamTrait},
|
|
9
|
-
Stream,
|
|
9
|
+
DeviceDescription, Stream,
|
|
10
10
|
};
|
|
11
11
|
use napi::{
|
|
12
12
|
bindgen_prelude::Float32Array,
|
|
@@ -24,6 +24,11 @@ const NUM_CHANNELS: u16 = 1;
|
|
|
24
24
|
const FRAME_SIZE: usize = (SAMPLE_RATE / 100) as usize; // 10 ms
|
|
25
25
|
const RING_SIZE: usize = FRAME_SIZE * 10000; // number of frames to store in the buffer. Corresponds to 100s of audio.
|
|
26
26
|
|
|
27
|
+
// output channels the mono audio is played on, stored in `AudioHardware::output_channel`
|
|
28
|
+
const CHANNEL_MONO: u8 = 0;
|
|
29
|
+
const CHANNEL_LEFT: u8 = 1;
|
|
30
|
+
const CHANNEL_RIGHT: u8 = 2;
|
|
31
|
+
|
|
27
32
|
#[napi]
|
|
28
33
|
pub struct AudioHardware {
|
|
29
34
|
host: cpal::Host,
|
|
@@ -32,6 +37,7 @@ pub struct AudioHardware {
|
|
|
32
37
|
input_callback: Option<Arc<ThreadsafeFunction<Float32Array>>>,
|
|
33
38
|
output_producer: Option<<HeapRb<f32> as Split>::Prod>,
|
|
34
39
|
output_to_discard: Arc<AtomicUsize>,
|
|
40
|
+
output_channel: Arc<AtomicU8>,
|
|
35
41
|
apm: Arc<Mutex<AudioProcessing>>,
|
|
36
42
|
}
|
|
37
43
|
|
|
@@ -74,6 +80,7 @@ impl AudioHardware {
|
|
|
74
80
|
input_callback: None,
|
|
75
81
|
output_producer: None,
|
|
76
82
|
output_to_discard: Arc::new(AtomicUsize::new(0)),
|
|
83
|
+
output_channel: Arc::new(AtomicU8::new(CHANNEL_MONO)),
|
|
77
84
|
apm: Arc::new(Mutex::new(apm)),
|
|
78
85
|
}
|
|
79
86
|
}
|
|
@@ -120,7 +127,7 @@ impl AudioHardware {
|
|
|
120
127
|
let id = device.id().ok()?;
|
|
121
128
|
let desc = device.description().ok()?;
|
|
122
129
|
Some(DeviceProperties {
|
|
123
|
-
name: desc
|
|
130
|
+
name: device_name(&desc),
|
|
124
131
|
id: id.to_string(),
|
|
125
132
|
input: desc.supports_input(),
|
|
126
133
|
output: desc.supports_output(),
|
|
@@ -144,8 +151,16 @@ impl AudioHardware {
|
|
|
144
151
|
let id = cfg.device_id.parse()?;
|
|
145
152
|
let device = self.host.device_by_id(&id).context("device unavailable")?;
|
|
146
153
|
|
|
154
|
+
// the stream has the channels of the device, so the mono audio can be played on the
|
|
155
|
+
// left or right channel only.
|
|
156
|
+
let channels = device
|
|
157
|
+
.default_output_config()
|
|
158
|
+
.map(|c| c.channels())
|
|
159
|
+
.unwrap_or(NUM_CHANNELS)
|
|
160
|
+
.max(NUM_CHANNELS);
|
|
161
|
+
|
|
147
162
|
let cpal_config = cpal::StreamConfig {
|
|
148
|
-
channels
|
|
163
|
+
channels,
|
|
149
164
|
sample_rate: SAMPLE_RATE,
|
|
150
165
|
buffer_size: cpal::BufferSize::Fixed(FRAME_SIZE as u32),
|
|
151
166
|
};
|
|
@@ -153,24 +168,48 @@ impl AudioHardware {
|
|
|
153
168
|
let (output_producer, mut output_consumer) = HeapRb::<f32>::new(RING_SIZE).split();
|
|
154
169
|
|
|
155
170
|
let samples_to_discard = self.output_to_discard.clone();
|
|
171
|
+
let output_channel = self.output_channel.clone();
|
|
156
172
|
let apm = self.apm.clone();
|
|
173
|
+
let channels = channels as usize;
|
|
174
|
+
let mut mono = vec![0.0f32; FRAME_SIZE];
|
|
157
175
|
|
|
158
176
|
let stream = device
|
|
159
177
|
.build_output_stream(
|
|
160
178
|
&cpal_config,
|
|
161
179
|
move |data: &mut [f32], _info: &cpal::OutputCallbackInfo| {
|
|
180
|
+
let frames = data.len() / channels;
|
|
181
|
+
mono.resize(frames, 0f32);
|
|
182
|
+
|
|
162
183
|
let discard = samples_to_discard.load(Ordering::Acquire);
|
|
163
184
|
if discard > 0 {
|
|
164
185
|
let skip = discard.min(output_consumer.occupied_len());
|
|
165
186
|
output_consumer.skip(skip);
|
|
166
187
|
samples_to_discard.fetch_sub(skip, Ordering::Release);
|
|
167
188
|
}
|
|
168
|
-
let len = output_consumer.pop_slice(
|
|
169
|
-
|
|
189
|
+
let len = output_consumer.pop_slice(&mut mono);
|
|
190
|
+
mono[len..].fill(0f32);
|
|
191
|
+
|
|
192
|
+
// mono plays on every channel (surround too), and so does a mono device
|
|
193
|
+
// whatever the selected channel. Left and right are the first two channels.
|
|
194
|
+
let only = match output_channel.load(Ordering::Relaxed) {
|
|
195
|
+
_ if channels == 1 => None,
|
|
196
|
+
CHANNEL_LEFT => Some(0),
|
|
197
|
+
CHANNEL_RIGHT => Some(1),
|
|
198
|
+
_ => None,
|
|
199
|
+
};
|
|
200
|
+
for (frame, &sample) in data.chunks_exact_mut(channels).zip(mono.iter()) {
|
|
201
|
+
match only {
|
|
202
|
+
Some(channel) => {
|
|
203
|
+
frame.fill(0f32);
|
|
204
|
+
frame[channel] = sample;
|
|
205
|
+
}
|
|
206
|
+
None => frame.fill(sample),
|
|
207
|
+
}
|
|
208
|
+
}
|
|
170
209
|
|
|
171
210
|
// Tell AEC what we are sending to the speakers.
|
|
172
211
|
if let Ok(mut apm) = apm.lock() {
|
|
173
|
-
apm.process_render_f32(&[&
|
|
212
|
+
apm.process_render_f32(&[&mono], &mut []).ok();
|
|
174
213
|
}
|
|
175
214
|
},
|
|
176
215
|
|err| eprintln!("output stream error: {err}"),
|
|
@@ -193,8 +232,16 @@ impl AudioHardware {
|
|
|
193
232
|
let id = cfg.device_id.parse()?;
|
|
194
233
|
let device = self.host.device_by_id(&id).context("device unavailable")?;
|
|
195
234
|
|
|
235
|
+
// the stream has the channels of the device and is downmixed here: some multi-channel
|
|
236
|
+
// devices deliver silence when Windows converts their input to mono.
|
|
237
|
+
let channels = device
|
|
238
|
+
.default_input_config()
|
|
239
|
+
.map(|c| c.channels())
|
|
240
|
+
.unwrap_or(NUM_CHANNELS)
|
|
241
|
+
.max(NUM_CHANNELS);
|
|
242
|
+
|
|
196
243
|
let cpal_config = cpal::StreamConfig {
|
|
197
|
-
channels
|
|
244
|
+
channels,
|
|
198
245
|
sample_rate: SAMPLE_RATE,
|
|
199
246
|
buffer_size: cpal::BufferSize::Fixed(FRAME_SIZE as u32),
|
|
200
247
|
};
|
|
@@ -202,30 +249,43 @@ impl AudioHardware {
|
|
|
202
249
|
let callback = self.input_callback.clone();
|
|
203
250
|
|
|
204
251
|
let apm = self.apm.clone();
|
|
252
|
+
let channels = channels as usize;
|
|
253
|
+
// mono samples not processed yet, the device does not always deliver whole frames
|
|
254
|
+
let mut pending: Vec<f32> = Vec::with_capacity(FRAME_SIZE * 4);
|
|
205
255
|
let mut processed = vec![0.0f32; FRAME_SIZE];
|
|
206
256
|
|
|
207
257
|
let input_stream = device
|
|
208
258
|
.build_input_stream(
|
|
209
259
|
&cpal_config,
|
|
210
260
|
move |data: &[f32], _: &cpal::InputCallbackInfo| {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
261
|
+
let Some(cb) = &callback else {
|
|
262
|
+
return;
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
// downmix to mono the way Windows does, the average of the channels
|
|
266
|
+
pending.extend(
|
|
267
|
+
data.chunks_exact(channels)
|
|
268
|
+
.map(|frame| frame.iter().sum::<f32>() / channels as f32),
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
// the AEC works on frames of exactly FRAME_SIZE samples
|
|
272
|
+
while pending.len() >= FRAME_SIZE {
|
|
273
|
+
let frame = &pending[..FRAME_SIZE];
|
|
216
274
|
|
|
217
275
|
// in case AEC fails, we still have the unprocessed data in `processed`
|
|
218
|
-
processed.copy_from_slice(
|
|
276
|
+
processed.copy_from_slice(frame);
|
|
219
277
|
|
|
220
278
|
// Process capture (apply AEC to mic input).
|
|
221
279
|
if let Ok(mut apm) = apm.lock() {
|
|
222
|
-
apm.process_capture_f32(&[
|
|
280
|
+
apm.process_capture_f32(&[frame], &mut [&mut processed])
|
|
281
|
+
.ok();
|
|
223
282
|
}
|
|
224
283
|
|
|
225
284
|
cb.call(
|
|
226
285
|
Ok(Float32Array::from(processed.clone())),
|
|
227
286
|
ThreadsafeFunctionCallMode::NonBlocking,
|
|
228
287
|
);
|
|
288
|
+
pending.drain(..FRAME_SIZE);
|
|
229
289
|
}
|
|
230
290
|
},
|
|
231
291
|
|err| eprintln!("input stream error: {err}"),
|
|
@@ -249,8 +309,35 @@ impl AudioHardware {
|
|
|
249
309
|
}
|
|
250
310
|
}
|
|
251
311
|
|
|
312
|
+
/// Sets the output channels the audio is played on: "mono" (all channels), "left" or
|
|
313
|
+
/// "right". It applies to the current and future output streams, and is ignored by mono
|
|
314
|
+
/// devices.
|
|
315
|
+
#[napi]
|
|
316
|
+
pub fn set_output_channel(&self, channel: String) -> anyhow::Result<()> {
|
|
317
|
+
let value = match channel.as_str() {
|
|
318
|
+
"mono" => CHANNEL_MONO,
|
|
319
|
+
"left" => CHANNEL_LEFT,
|
|
320
|
+
"right" => CHANNEL_RIGHT,
|
|
321
|
+
_ => anyhow::bail!("unknown output channel: {channel}"),
|
|
322
|
+
};
|
|
323
|
+
self.output_channel.store(value, Ordering::Relaxed);
|
|
324
|
+
Ok(())
|
|
325
|
+
}
|
|
326
|
+
|
|
252
327
|
#[napi]
|
|
253
328
|
pub fn set_input_callback(&mut self, cb: Arc<ThreadsafeFunction<Float32Array>>) {
|
|
254
329
|
self.input_callback = Some(cb);
|
|
255
330
|
}
|
|
256
331
|
}
|
|
332
|
+
|
|
333
|
+
/// The name of a device as the OS shows it. On Windows that is the friendly name, e.g.
|
|
334
|
+
/// "Speakers (USB Audio Device)" rather than the "Speakers" of [`DeviceDescription::name`];
|
|
335
|
+
/// cpal puts it in the extended lines when it differs from the name. The launcher names the
|
|
336
|
+
/// devices the same way (`apps/launcher/src-tauri/src/audio_devices.rs` in convis).
|
|
337
|
+
fn device_name(desc: &DeviceDescription) -> String {
|
|
338
|
+
#[cfg(windows)]
|
|
339
|
+
if let Some(friendly_name) = desc.extended().first() {
|
|
340
|
+
return friendly_name.clone();
|
|
341
|
+
}
|
|
342
|
+
desc.name().to_string()
|
|
343
|
+
}
|