@matjash/pixi-native-win32-x64 0.1.2 → 0.2.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/THIRD_PARTY_NOTICES.md +7 -6
- package/native/audio/dist/win32-x64/native_audio.node +0 -0
- package/native/audio/src/audio_impl.rs +1249 -0
- package/native/audio/src/lib.rs +2 -1517
- package/native/gpu/dist/win32-x64/pixi_native_gpu.node +0 -0
- package/native/gpu/src/index.d.ts +3 -1
- package/native/window/dist/win32-x64/SDL3.dll +0 -0
- package/native/window/dist/win32-x64/native_window.node +0 -0
- package/native/window/src/index.d.ts +54 -8
- package/native/window/src/index.js +80 -11
- package/native/window/src/key-mapping.js +32 -0
- package/native/window/src/lib.rs +658 -46
- package/package.json +1 -1
package/native/audio/src/lib.rs
CHANGED
|
@@ -1,1520 +1,5 @@
|
|
|
1
1
|
#![deny(clippy::all)]
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
use std::io::Read;
|
|
5
|
-
use std::process::{Child, Command, Stdio};
|
|
6
|
-
use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, AtomicUsize, Ordering};
|
|
7
|
-
use std::sync::{Arc, Mutex, OnceLock};
|
|
8
|
-
use std::thread;
|
|
3
|
+
mod audio_impl;
|
|
9
4
|
|
|
10
|
-
use
|
|
11
|
-
use cpal::{FromSample, SampleFormat, SizedSample, Stream};
|
|
12
|
-
use crossbeam_queue::{ArrayQueue, SegQueue};
|
|
13
|
-
use napi::bindgen_prelude::*;
|
|
14
|
-
use napi::threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode};
|
|
15
|
-
use napi_derive::napi;
|
|
16
|
-
|
|
17
|
-
const CHANNELS: usize = 2;
|
|
18
|
-
const STREAM_CHUNK_FRAMES: usize = 2048;
|
|
19
|
-
const STREAM_CHUNK_SAMPLES: usize = STREAM_CHUNK_FRAMES * CHANNELS;
|
|
20
|
-
const STREAM_START_CHUNKS: usize = 4;
|
|
21
|
-
const STREAM_BUFFER_SECONDS: usize = 2;
|
|
22
|
-
type EventNotifier = ThreadsafeFunction<(), (), (), Status, false, true, 1>;
|
|
23
|
-
|
|
24
|
-
#[napi(object)]
|
|
25
|
-
#[derive(Clone)]
|
|
26
|
-
pub struct NativeVoiceOptions {
|
|
27
|
-
pub owner_id: u32,
|
|
28
|
-
pub id: u32,
|
|
29
|
-
pub source: String,
|
|
30
|
-
pub ffmpeg_path: String,
|
|
31
|
-
pub offset_seconds: f64,
|
|
32
|
-
pub duration_seconds: Option<f64>,
|
|
33
|
-
pub volume: f64,
|
|
34
|
-
pub muted: bool,
|
|
35
|
-
pub loop_: bool,
|
|
36
|
-
pub streaming: bool,
|
|
37
|
-
pub playback_rate: f64,
|
|
38
|
-
pub input_args: Vec<String>,
|
|
39
|
-
pub output_args: Vec<String>,
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
#[napi(object)]
|
|
43
|
-
pub struct NativeCommandOptions {
|
|
44
|
-
pub owner_id: u32,
|
|
45
|
-
pub command: String,
|
|
46
|
-
pub id: Option<u32>,
|
|
47
|
-
pub value: Option<f64>,
|
|
48
|
-
pub bool_value: Option<bool>,
|
|
49
|
-
pub from: Option<f64>,
|
|
50
|
-
pub to: Option<f64>,
|
|
51
|
-
pub duration_ms: Option<f64>,
|
|
52
|
-
pub fade_version: Option<u32>,
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
#[napi(object)]
|
|
56
|
-
pub struct NativeAudioEvent {
|
|
57
|
-
pub owner_id: u32,
|
|
58
|
-
pub event: String,
|
|
59
|
-
pub id: Option<u32>,
|
|
60
|
-
pub message: Option<String>,
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
#[napi(object)]
|
|
64
|
-
pub struct NativeAudioDiagnostics {
|
|
65
|
-
pub active_voices: u32,
|
|
66
|
-
pub queued_ms: f64,
|
|
67
|
-
pub underruns: u32,
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
struct QueuedEvent {
|
|
71
|
-
owner_id: u32,
|
|
72
|
-
event: &'static str,
|
|
73
|
-
id: Option<u32>,
|
|
74
|
-
message: Option<String>,
|
|
75
|
-
target_frame: u64,
|
|
76
|
-
sequence: u64,
|
|
77
|
-
fade_version: Option<u32>,
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
struct Fade {
|
|
81
|
-
from: f32,
|
|
82
|
-
to: f32,
|
|
83
|
-
start_frame: u64,
|
|
84
|
-
duration_frames: u64,
|
|
85
|
-
version: u32,
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
enum VoiceSource {
|
|
89
|
-
Loading,
|
|
90
|
-
Static(Arc<Vec<f32>>),
|
|
91
|
-
Streaming {
|
|
92
|
-
buffer: Arc<StreamingBuffer>,
|
|
93
|
-
chunk: Option<Box<[f32]>>,
|
|
94
|
-
offset: usize,
|
|
95
|
-
},
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
struct VoiceSnapshot {
|
|
99
|
-
owner_id: u32,
|
|
100
|
-
time_bits: AtomicU64,
|
|
101
|
-
volume_bits: AtomicU32,
|
|
102
|
-
playing: AtomicBool,
|
|
103
|
-
stream: Option<Arc<StreamingBuffer>>,
|
|
104
|
-
current_chunk_samples: AtomicUsize,
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
struct Voice {
|
|
108
|
-
owner_id: u32,
|
|
109
|
-
id: u32,
|
|
110
|
-
source: VoiceSource,
|
|
111
|
-
offset_seconds: f64,
|
|
112
|
-
position_frames: f64,
|
|
113
|
-
rendered_frames: u64,
|
|
114
|
-
volume: f32,
|
|
115
|
-
muted: bool,
|
|
116
|
-
looped: bool,
|
|
117
|
-
playing: bool,
|
|
118
|
-
playback_rate: f64,
|
|
119
|
-
fade: Option<Fade>,
|
|
120
|
-
play_announced: bool,
|
|
121
|
-
finished: bool,
|
|
122
|
-
snapshot: Arc<VoiceSnapshot>,
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
struct StreamingBuffer {
|
|
126
|
-
chunks: ArrayQueue<Box<[f32]>>,
|
|
127
|
-
queued_samples: AtomicUsize,
|
|
128
|
-
produced_chunks: AtomicUsize,
|
|
129
|
-
ready: AtomicBool,
|
|
130
|
-
ended: AtomicBool,
|
|
131
|
-
stopped: AtomicBool,
|
|
132
|
-
producer_waiting: AtomicBool,
|
|
133
|
-
producer_thread: OnceLock<thread::Thread>,
|
|
134
|
-
child: Mutex<Option<Child>>,
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
impl StreamingBuffer {
|
|
138
|
-
fn new(sample_rate: u32) -> Self {
|
|
139
|
-
let capacity = (sample_rate as usize * STREAM_BUFFER_SECONDS).div_ceil(STREAM_CHUNK_FRAMES);
|
|
140
|
-
Self {
|
|
141
|
-
chunks: ArrayQueue::new(capacity),
|
|
142
|
-
queued_samples: AtomicUsize::new(0),
|
|
143
|
-
produced_chunks: AtomicUsize::new(0),
|
|
144
|
-
ready: AtomicBool::new(false),
|
|
145
|
-
ended: AtomicBool::new(false),
|
|
146
|
-
stopped: AtomicBool::new(false),
|
|
147
|
-
producer_waiting: AtomicBool::new(false),
|
|
148
|
-
producer_thread: OnceLock::new(),
|
|
149
|
-
child: Mutex::new(None),
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
fn push(&self, mut chunk: Box<[f32]>) -> bool {
|
|
154
|
-
let _ = self.producer_thread.set(thread::current());
|
|
155
|
-
loop {
|
|
156
|
-
if self.stopped.load(Ordering::Acquire) {
|
|
157
|
-
return false;
|
|
158
|
-
}
|
|
159
|
-
match self.chunks.push(chunk) {
|
|
160
|
-
Ok(()) => {
|
|
161
|
-
self.queued_samples
|
|
162
|
-
.fetch_add(STREAM_CHUNK_SAMPLES, Ordering::Release);
|
|
163
|
-
self.produced_chunks.fetch_add(1, Ordering::Relaxed);
|
|
164
|
-
if self.chunks.len() >= STREAM_START_CHUNKS {
|
|
165
|
-
self.ready.store(true, Ordering::Release);
|
|
166
|
-
}
|
|
167
|
-
return true;
|
|
168
|
-
}
|
|
169
|
-
Err(returned) => {
|
|
170
|
-
chunk = returned;
|
|
171
|
-
self.producer_waiting.store(true, Ordering::Release);
|
|
172
|
-
if self.chunks.is_full() {
|
|
173
|
-
thread::park();
|
|
174
|
-
}
|
|
175
|
-
self.producer_waiting.store(false, Ordering::Release);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
fn pop(&self) -> Option<Box<[f32]>> {
|
|
182
|
-
let chunk = self.chunks.pop()?;
|
|
183
|
-
self.queued_samples
|
|
184
|
-
.fetch_sub(chunk.len(), Ordering::Release);
|
|
185
|
-
if self.producer_waiting.swap(false, Ordering::AcqRel) {
|
|
186
|
-
if let Some(producer) = self.producer_thread.get() {
|
|
187
|
-
producer.unpark();
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
Some(chunk)
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
fn queued_samples(&self) -> usize {
|
|
194
|
-
self.queued_samples.load(Ordering::Acquire)
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
fn stop(&self) {
|
|
198
|
-
self.stopped.store(true, Ordering::Release);
|
|
199
|
-
if let Some(producer) = self.producer_thread.get() {
|
|
200
|
-
producer.unpark();
|
|
201
|
-
}
|
|
202
|
-
if let Ok(mut child) = self.child.lock() {
|
|
203
|
-
if let Some(child) = child.as_mut() {
|
|
204
|
-
let _ = child.kill();
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
enum MixerCommand {
|
|
211
|
-
AddVoice(Voice),
|
|
212
|
-
AttachStatic { id: u32, samples: Arc<Vec<f32>> },
|
|
213
|
-
Control(NativeCommandOptions),
|
|
214
|
-
RemoveOwner(u32),
|
|
215
|
-
StopAll,
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
#[derive(Default)]
|
|
219
|
-
struct Mixer {
|
|
220
|
-
voices: BTreeMap<u32, Voice>,
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
struct SharedState {
|
|
224
|
-
sample_rate: u32,
|
|
225
|
-
output_channels: usize,
|
|
226
|
-
commands: SegQueue<MixerCommand>,
|
|
227
|
-
snapshots: Mutex<HashMap<u32, Arc<VoiceSnapshot>>>,
|
|
228
|
-
completed_voices: SegQueue<u32>,
|
|
229
|
-
cache: Mutex<HashMap<String, Arc<Vec<f32>>>>,
|
|
230
|
-
events: SegQueue<QueuedEvent>,
|
|
231
|
-
event_notifier: Option<Arc<EventNotifier>>,
|
|
232
|
-
event_sequence: AtomicU64,
|
|
233
|
-
audible_frame: AtomicU64,
|
|
234
|
-
global_volume_bits: AtomicU32,
|
|
235
|
-
global_muted: AtomicBool,
|
|
236
|
-
fade_versions: Mutex<HashMap<u32, u32>>,
|
|
237
|
-
underruns: AtomicU32,
|
|
238
|
-
shutdown: AtomicBool,
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
impl SharedState {
|
|
242
|
-
fn queue_event(
|
|
243
|
-
&self,
|
|
244
|
-
owner_id: u32,
|
|
245
|
-
event: &'static str,
|
|
246
|
-
id: Option<u32>,
|
|
247
|
-
message: Option<String>,
|
|
248
|
-
fade_version: Option<u32>,
|
|
249
|
-
) {
|
|
250
|
-
self.events.push(QueuedEvent {
|
|
251
|
-
owner_id,
|
|
252
|
-
event,
|
|
253
|
-
id,
|
|
254
|
-
message,
|
|
255
|
-
target_frame: self.audible_frame.load(Ordering::Acquire),
|
|
256
|
-
sequence: self.event_sequence.fetch_add(1, Ordering::Relaxed),
|
|
257
|
-
fade_version,
|
|
258
|
-
});
|
|
259
|
-
if let Some(notifier) = &self.event_notifier {
|
|
260
|
-
notifier.call((), ThreadsafeFunctionCallMode::NonBlocking);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
fn stop_snapshot(snapshot: &VoiceSnapshot) {
|
|
265
|
-
if let Some(stream) = &snapshot.stream {
|
|
266
|
-
stream.stop();
|
|
267
|
-
}
|
|
268
|
-
snapshot.playing.store(false, Ordering::Release);
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
#[napi]
|
|
273
|
-
pub struct NativeAudioEngine {
|
|
274
|
-
state: Arc<SharedState>,
|
|
275
|
-
stream: Mutex<Option<Stream>>,
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
#[napi]
|
|
279
|
-
impl NativeAudioEngine {
|
|
280
|
-
#[napi(constructor)]
|
|
281
|
-
pub fn new(event_notifier: EventNotifier) -> Result<Self> {
|
|
282
|
-
let host = cpal::default_host();
|
|
283
|
-
let device = host.default_output_device().ok_or_else(|| {
|
|
284
|
-
Error::from_reason("Windows has no default WASAPI audio output device")
|
|
285
|
-
})?;
|
|
286
|
-
let supported = device
|
|
287
|
-
.default_output_config()
|
|
288
|
-
.map_err(|error| Error::from_reason(format!("Cannot query WASAPI output: {error}")))?;
|
|
289
|
-
let sample_format = supported.sample_format();
|
|
290
|
-
let config = supported.config();
|
|
291
|
-
let state = Arc::new(SharedState {
|
|
292
|
-
sample_rate: config.sample_rate.0,
|
|
293
|
-
output_channels: config.channels as usize,
|
|
294
|
-
commands: SegQueue::new(),
|
|
295
|
-
snapshots: Mutex::new(HashMap::new()),
|
|
296
|
-
completed_voices: SegQueue::new(),
|
|
297
|
-
cache: Mutex::new(HashMap::new()),
|
|
298
|
-
events: SegQueue::new(),
|
|
299
|
-
event_notifier: Some(Arc::new(event_notifier)),
|
|
300
|
-
event_sequence: AtomicU64::new(0),
|
|
301
|
-
audible_frame: AtomicU64::new(0),
|
|
302
|
-
global_volume_bits: AtomicU32::new(1.0_f32.to_bits()),
|
|
303
|
-
global_muted: AtomicBool::new(false),
|
|
304
|
-
fade_versions: Mutex::new(HashMap::new()),
|
|
305
|
-
underruns: AtomicU32::new(0),
|
|
306
|
-
shutdown: AtomicBool::new(false),
|
|
307
|
-
});
|
|
308
|
-
let stream_state = Arc::clone(&state);
|
|
309
|
-
let error_state = Arc::clone(&state);
|
|
310
|
-
let error_callback = move |error: cpal::StreamError| {
|
|
311
|
-
error_state.queue_event(0, "playerror", None, Some(error.to_string()), None);
|
|
312
|
-
};
|
|
313
|
-
let stream = match sample_format {
|
|
314
|
-
SampleFormat::F32 => {
|
|
315
|
-
build_stream::<f32>(&device, &config, stream_state, error_callback)
|
|
316
|
-
}
|
|
317
|
-
SampleFormat::I16 => {
|
|
318
|
-
build_stream::<i16>(&device, &config, stream_state, error_callback)
|
|
319
|
-
}
|
|
320
|
-
SampleFormat::U16 => {
|
|
321
|
-
build_stream::<u16>(&device, &config, stream_state, error_callback)
|
|
322
|
-
}
|
|
323
|
-
format => Err(Error::from_reason(format!(
|
|
324
|
-
"Unsupported WASAPI sample format: {format:?}"
|
|
325
|
-
))),
|
|
326
|
-
}?;
|
|
327
|
-
stream
|
|
328
|
-
.play()
|
|
329
|
-
.map_err(|error| Error::from_reason(format!("Cannot start WASAPI output: {error}")))?;
|
|
330
|
-
Ok(Self {
|
|
331
|
-
state,
|
|
332
|
-
stream: Mutex::new(Some(stream)),
|
|
333
|
-
})
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
#[napi]
|
|
337
|
-
pub fn create_voice(&self, options: NativeVoiceOptions) -> Result<()> {
|
|
338
|
-
validate_voice_options(&options)?;
|
|
339
|
-
let id = options.id;
|
|
340
|
-
let owner_id = options.owner_id;
|
|
341
|
-
let stream = options
|
|
342
|
-
.streaming
|
|
343
|
-
.then(|| Arc::new(StreamingBuffer::new(self.state.sample_rate)));
|
|
344
|
-
let snapshot = Arc::new(VoiceSnapshot {
|
|
345
|
-
owner_id,
|
|
346
|
-
time_bits: AtomicU64::new(options.offset_seconds.to_bits()),
|
|
347
|
-
volume_bits: AtomicU32::new((options.volume as f32).to_bits()),
|
|
348
|
-
playing: AtomicBool::new(true),
|
|
349
|
-
stream: stream.clone(),
|
|
350
|
-
current_chunk_samples: AtomicUsize::new(0),
|
|
351
|
-
});
|
|
352
|
-
let voice = Voice {
|
|
353
|
-
owner_id,
|
|
354
|
-
id,
|
|
355
|
-
source: stream
|
|
356
|
-
.as_ref()
|
|
357
|
-
.map_or(VoiceSource::Loading, |buffer| VoiceSource::Streaming {
|
|
358
|
-
buffer: Arc::clone(buffer),
|
|
359
|
-
chunk: None,
|
|
360
|
-
offset: 0,
|
|
361
|
-
}),
|
|
362
|
-
offset_seconds: options.offset_seconds,
|
|
363
|
-
position_frames: 0.0,
|
|
364
|
-
rendered_frames: 0,
|
|
365
|
-
volume: options.volume as f32,
|
|
366
|
-
muted: options.muted,
|
|
367
|
-
looped: options.loop_ && !options.streaming,
|
|
368
|
-
playing: true,
|
|
369
|
-
playback_rate: options.playback_rate,
|
|
370
|
-
fade: None,
|
|
371
|
-
play_announced: false,
|
|
372
|
-
finished: false,
|
|
373
|
-
snapshot: Arc::clone(&snapshot),
|
|
374
|
-
};
|
|
375
|
-
self.state
|
|
376
|
-
.snapshots
|
|
377
|
-
.lock()
|
|
378
|
-
.map_err(|_| Error::from_reason("Audio mixer state is poisoned"))?
|
|
379
|
-
.insert(id, snapshot);
|
|
380
|
-
self.state.commands.push(MixerCommand::AddVoice(voice));
|
|
381
|
-
|
|
382
|
-
if let Some(stream) = stream {
|
|
383
|
-
start_streaming_decode(Arc::clone(&self.state), options, stream);
|
|
384
|
-
} else {
|
|
385
|
-
start_static_decode(Arc::clone(&self.state), options, false, None);
|
|
386
|
-
}
|
|
387
|
-
Ok(())
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
#[napi]
|
|
391
|
-
pub fn preload(
|
|
392
|
-
&self,
|
|
393
|
-
owner_id: u32,
|
|
394
|
-
request_id: u32,
|
|
395
|
-
source: String,
|
|
396
|
-
ffmpeg_path: String,
|
|
397
|
-
offset_seconds: f64,
|
|
398
|
-
duration_seconds: Option<f64>,
|
|
399
|
-
) -> Result<()> {
|
|
400
|
-
if offset_seconds < 0.0 || duration_seconds.is_some_and(|value| value <= 0.0) {
|
|
401
|
-
return Err(Error::from_reason("Invalid audio preload range"));
|
|
402
|
-
}
|
|
403
|
-
start_static_decode(
|
|
404
|
-
Arc::clone(&self.state),
|
|
405
|
-
NativeVoiceOptions {
|
|
406
|
-
owner_id,
|
|
407
|
-
id: 0,
|
|
408
|
-
source,
|
|
409
|
-
ffmpeg_path,
|
|
410
|
-
offset_seconds,
|
|
411
|
-
duration_seconds,
|
|
412
|
-
volume: 1.0,
|
|
413
|
-
muted: false,
|
|
414
|
-
loop_: false,
|
|
415
|
-
streaming: false,
|
|
416
|
-
playback_rate: 1.0,
|
|
417
|
-
input_args: Vec::new(),
|
|
418
|
-
output_args: Vec::new(),
|
|
419
|
-
},
|
|
420
|
-
true,
|
|
421
|
-
Some(request_id),
|
|
422
|
-
);
|
|
423
|
-
Ok(())
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
#[napi]
|
|
427
|
-
pub fn command(&self, options: NativeCommandOptions) -> Result<()> {
|
|
428
|
-
match options.command.as_str() {
|
|
429
|
-
"play" | "pause" | "mute" | "loop" | "stop" => {}
|
|
430
|
-
"volume" => {
|
|
431
|
-
required_unit_value(options.value, "volume")?;
|
|
432
|
-
}
|
|
433
|
-
"seek" => {
|
|
434
|
-
let seconds = options.value.unwrap_or(0.0);
|
|
435
|
-
if !seconds.is_finite() || seconds < 0.0 {
|
|
436
|
-
return Err(Error::from_reason(
|
|
437
|
-
"Audio seek must be non-negative and finite",
|
|
438
|
-
));
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
"fade" => {
|
|
442
|
-
required_unit_value(options.from, "fade start")?;
|
|
443
|
-
required_unit_value(options.to, "fade target")?;
|
|
444
|
-
let duration_ms = options.duration_ms.unwrap_or(0.0);
|
|
445
|
-
if !duration_ms.is_finite() || duration_ms < 0.0 {
|
|
446
|
-
return Err(Error::from_reason(
|
|
447
|
-
"Fade duration must be non-negative and finite",
|
|
448
|
-
));
|
|
449
|
-
}
|
|
450
|
-
if let Some(id) = options.id {
|
|
451
|
-
if let Ok(mut versions) = self.state.fade_versions.lock() {
|
|
452
|
-
versions.insert(id, options.fade_version.unwrap_or(0));
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
command => {
|
|
457
|
-
return Err(Error::from_reason(format!(
|
|
458
|
-
"Unknown audio command: {command}"
|
|
459
|
-
)))
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
if options.command == "stop" {
|
|
464
|
-
self.remove_snapshots(options.owner_id, options.id);
|
|
465
|
-
}
|
|
466
|
-
self.state.commands.push(MixerCommand::Control(options));
|
|
467
|
-
Ok(())
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
#[napi]
|
|
471
|
-
pub fn unload_owner(&self, owner_id: u32) {
|
|
472
|
-
self.remove_snapshots(owner_id, None);
|
|
473
|
-
self.state
|
|
474
|
-
.commands
|
|
475
|
-
.push(MixerCommand::RemoveOwner(owner_id));
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
#[napi]
|
|
479
|
-
pub fn current_time(&self, id: u32) -> Option<f64> {
|
|
480
|
-
let snapshot = self.state.snapshots.lock().ok()?.get(&id).cloned()?;
|
|
481
|
-
Some(f64::from_bits(snapshot.time_bits.load(Ordering::Acquire)))
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
#[napi]
|
|
485
|
-
pub fn current_volume(&self, id: u32) -> Option<f64> {
|
|
486
|
-
let snapshot = self.state.snapshots.lock().ok()?.get(&id).cloned()?;
|
|
487
|
-
Some(f32::from_bits(snapshot.volume_bits.load(Ordering::Acquire)) as f64)
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
#[napi]
|
|
491
|
-
pub fn set_global_volume(&self, value: f64) -> Result<()> {
|
|
492
|
-
let value = required_unit_value(Some(value), "global volume")? as f32;
|
|
493
|
-
self.state
|
|
494
|
-
.global_volume_bits
|
|
495
|
-
.store(value.to_bits(), Ordering::Release);
|
|
496
|
-
Ok(())
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
#[napi]
|
|
500
|
-
pub fn set_global_muted(&self, value: bool) {
|
|
501
|
-
self.state.global_muted.store(value, Ordering::Release);
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
#[napi]
|
|
505
|
-
pub fn drain_events(&self) -> Vec<NativeAudioEvent> {
|
|
506
|
-
if let Ok(mut snapshots) = self.state.snapshots.lock() {
|
|
507
|
-
while let Some(id) = self.state.completed_voices.pop() {
|
|
508
|
-
snapshots.remove(&id);
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
let mut queued = Vec::new();
|
|
512
|
-
while let Some(event) = self.state.events.pop() {
|
|
513
|
-
queued.push(event);
|
|
514
|
-
}
|
|
515
|
-
queued.sort_by_key(|event| (event.target_frame, event.sequence));
|
|
516
|
-
let fade_versions = self.state.fade_versions.lock().ok();
|
|
517
|
-
queued
|
|
518
|
-
.into_iter()
|
|
519
|
-
.filter(|event| {
|
|
520
|
-
let Some(version) = event.fade_version else {
|
|
521
|
-
return true;
|
|
522
|
-
};
|
|
523
|
-
event.id.is_some_and(|id| {
|
|
524
|
-
fade_versions
|
|
525
|
-
.as_ref()
|
|
526
|
-
.and_then(|versions| versions.get(&id))
|
|
527
|
-
.is_some_and(|current| *current == version)
|
|
528
|
-
})
|
|
529
|
-
})
|
|
530
|
-
.map(|event| NativeAudioEvent {
|
|
531
|
-
owner_id: event.owner_id,
|
|
532
|
-
event: event.event.to_string(),
|
|
533
|
-
id: event.id,
|
|
534
|
-
message: event.message,
|
|
535
|
-
})
|
|
536
|
-
.collect()
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
#[napi]
|
|
540
|
-
pub fn diagnostics(&self) -> NativeAudioDiagnostics {
|
|
541
|
-
let (active_voices, queued_samples) = self
|
|
542
|
-
.state
|
|
543
|
-
.snapshots
|
|
544
|
-
.lock()
|
|
545
|
-
.map(|snapshots| {
|
|
546
|
-
let active = snapshots
|
|
547
|
-
.values()
|
|
548
|
-
.filter(|snapshot| snapshot.playing.load(Ordering::Acquire))
|
|
549
|
-
.count() as u32;
|
|
550
|
-
let queued = snapshots
|
|
551
|
-
.values()
|
|
552
|
-
.map(|snapshot| {
|
|
553
|
-
snapshot
|
|
554
|
-
.stream
|
|
555
|
-
.as_ref()
|
|
556
|
-
.map_or(0, |stream| stream.queued_samples())
|
|
557
|
-
+ snapshot.current_chunk_samples.load(Ordering::Acquire)
|
|
558
|
-
})
|
|
559
|
-
.sum::<usize>();
|
|
560
|
-
(active, queued)
|
|
561
|
-
})
|
|
562
|
-
.unwrap_or((0, 0));
|
|
563
|
-
NativeAudioDiagnostics {
|
|
564
|
-
active_voices,
|
|
565
|
-
queued_ms: queued_samples as f64 * 1000.0
|
|
566
|
-
/ (self.state.sample_rate as f64 * CHANNELS as f64),
|
|
567
|
-
underruns: self.state.underruns.load(Ordering::Relaxed),
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
#[napi]
|
|
572
|
-
pub fn stop_all(&self) {
|
|
573
|
-
if let Ok(mut snapshots) = self.state.snapshots.lock() {
|
|
574
|
-
for snapshot in snapshots.values() {
|
|
575
|
-
SharedState::stop_snapshot(snapshot);
|
|
576
|
-
}
|
|
577
|
-
snapshots.clear();
|
|
578
|
-
}
|
|
579
|
-
self.state.commands.push(MixerCommand::StopAll);
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
#[napi]
|
|
583
|
-
pub fn shutdown(&self) {
|
|
584
|
-
self.state.shutdown.store(true, Ordering::Release);
|
|
585
|
-
self.stop_all();
|
|
586
|
-
if let Ok(mut stream) = self.stream.lock() {
|
|
587
|
-
stream.take();
|
|
588
|
-
}
|
|
589
|
-
if let Ok(mut cache) = self.state.cache.lock() {
|
|
590
|
-
cache.clear();
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
fn remove_snapshots(&self, owner_id: u32, id: Option<u32>) {
|
|
595
|
-
if let Ok(mut snapshots) = self.state.snapshots.lock() {
|
|
596
|
-
let ids: Vec<u32> = snapshots
|
|
597
|
-
.iter()
|
|
598
|
-
.filter(|(voice_id, snapshot)| {
|
|
599
|
-
snapshot.owner_id == owner_id && id.is_none_or(|id| id == **voice_id)
|
|
600
|
-
})
|
|
601
|
-
.map(|(id, _)| *id)
|
|
602
|
-
.collect();
|
|
603
|
-
for id in ids {
|
|
604
|
-
if let Some(snapshot) = snapshots.remove(&id) {
|
|
605
|
-
SharedState::stop_snapshot(&snapshot);
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
impl Drop for NativeAudioEngine {
|
|
613
|
-
fn drop(&mut self) {
|
|
614
|
-
self.state.shutdown.store(true, Ordering::Release);
|
|
615
|
-
if let Ok(snapshots) = self.state.snapshots.lock() {
|
|
616
|
-
for snapshot in snapshots.values() {
|
|
617
|
-
SharedState::stop_snapshot(snapshot);
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
impl Mixer {
|
|
624
|
-
fn apply_commands(&mut self, state: &SharedState) {
|
|
625
|
-
while let Some(command) = state.commands.pop() {
|
|
626
|
-
match command {
|
|
627
|
-
MixerCommand::AddVoice(voice) => {
|
|
628
|
-
self.voices.insert(voice.id, voice);
|
|
629
|
-
}
|
|
630
|
-
MixerCommand::AttachStatic { id, samples } => {
|
|
631
|
-
if let Some(voice) = self.voices.get_mut(&id) {
|
|
632
|
-
voice.source = VoiceSource::Static(samples);
|
|
633
|
-
voice.play_announced = true;
|
|
634
|
-
state.queue_event(voice.owner_id, "play", Some(id), None, None);
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
MixerCommand::Control(options) => self.apply_control(state, options),
|
|
638
|
-
MixerCommand::RemoveOwner(owner_id) => {
|
|
639
|
-
self.voices.retain(|_, voice| voice.owner_id != owner_id);
|
|
640
|
-
}
|
|
641
|
-
MixerCommand::StopAll => {
|
|
642
|
-
self.voices.clear();
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
fn apply_control(&mut self, state: &SharedState, options: NativeCommandOptions) {
|
|
649
|
-
if options.command == "stop" {
|
|
650
|
-
self.voices.retain(|id, voice| {
|
|
651
|
-
let matches = voice.owner_id == options.owner_id
|
|
652
|
-
&& options.id.is_none_or(|requested| requested == *id);
|
|
653
|
-
if matches {
|
|
654
|
-
state.queue_event(voice.owner_id, "stop", Some(*id), None, None);
|
|
655
|
-
}
|
|
656
|
-
!matches
|
|
657
|
-
});
|
|
658
|
-
return;
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
for (id, voice) in &mut self.voices {
|
|
662
|
-
if voice.owner_id != options.owner_id
|
|
663
|
-
|| options.id.is_some_and(|requested| requested != *id)
|
|
664
|
-
{
|
|
665
|
-
continue;
|
|
666
|
-
}
|
|
667
|
-
Self::apply_voice_control(state, *id, voice, &options);
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
fn apply_voice_control(
|
|
672
|
-
state: &SharedState,
|
|
673
|
-
id: u32,
|
|
674
|
-
voice: &mut Voice,
|
|
675
|
-
options: &NativeCommandOptions,
|
|
676
|
-
) {
|
|
677
|
-
match options.command.as_str() {
|
|
678
|
-
"play" => {
|
|
679
|
-
voice.playing = true;
|
|
680
|
-
voice.snapshot.playing.store(true, Ordering::Release);
|
|
681
|
-
state.queue_event(voice.owner_id, "play", Some(id), None, None);
|
|
682
|
-
}
|
|
683
|
-
"pause" => {
|
|
684
|
-
voice.playing = false;
|
|
685
|
-
voice.snapshot.playing.store(false, Ordering::Release);
|
|
686
|
-
state.queue_event(voice.owner_id, "pause", Some(id), None, None);
|
|
687
|
-
}
|
|
688
|
-
"volume" => {
|
|
689
|
-
voice.volume = options.value.unwrap_or(1.0) as f32;
|
|
690
|
-
voice.fade = None;
|
|
691
|
-
voice
|
|
692
|
-
.snapshot
|
|
693
|
-
.volume_bits
|
|
694
|
-
.store(voice.volume.to_bits(), Ordering::Release);
|
|
695
|
-
state.queue_event(voice.owner_id, "volume", Some(id), None, None);
|
|
696
|
-
}
|
|
697
|
-
"mute" => {
|
|
698
|
-
voice.muted = options.bool_value.unwrap_or(false);
|
|
699
|
-
state.queue_event(voice.owner_id, "mute", Some(id), None, None);
|
|
700
|
-
}
|
|
701
|
-
"loop" => voice.looped = options.bool_value.unwrap_or(false),
|
|
702
|
-
"seek" => {
|
|
703
|
-
let seconds = options.value.unwrap_or(0.0);
|
|
704
|
-
voice.position_frames = seconds * state.sample_rate as f64;
|
|
705
|
-
voice.rendered_frames = (seconds * state.sample_rate as f64) as u64;
|
|
706
|
-
voice.fade = None;
|
|
707
|
-
state.queue_event(voice.owner_id, "seek", Some(id), None, None);
|
|
708
|
-
}
|
|
709
|
-
"fade" => {
|
|
710
|
-
voice.fade = Some(Fade {
|
|
711
|
-
from: voice.volume,
|
|
712
|
-
to: options.to.unwrap_or(1.0) as f32,
|
|
713
|
-
start_frame: voice.rendered_frames,
|
|
714
|
-
duration_frames: (options.duration_ms.unwrap_or(0.0) * state.sample_rate as f64
|
|
715
|
-
/ 1000.0) as u64,
|
|
716
|
-
version: options.fade_version.unwrap_or(0),
|
|
717
|
-
});
|
|
718
|
-
}
|
|
719
|
-
_ => {}
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
enum VoiceFrame {
|
|
725
|
-
Sample(f32, f32, bool),
|
|
726
|
-
Pending,
|
|
727
|
-
Starved,
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
fn build_stream<T>(
|
|
731
|
-
device: &cpal::Device,
|
|
732
|
-
config: &cpal::StreamConfig,
|
|
733
|
-
state: Arc<SharedState>,
|
|
734
|
-
error_callback: impl FnMut(cpal::StreamError) + Send + 'static,
|
|
735
|
-
) -> Result<Stream>
|
|
736
|
-
where
|
|
737
|
-
T: SizedSample + FromSample<f32>,
|
|
738
|
-
{
|
|
739
|
-
let mut mixer = Mixer::default();
|
|
740
|
-
device
|
|
741
|
-
.build_output_stream(
|
|
742
|
-
config,
|
|
743
|
-
move |output: &mut [T], _| render_output(output, &state, &mut mixer),
|
|
744
|
-
error_callback,
|
|
745
|
-
None,
|
|
746
|
-
)
|
|
747
|
-
.map_err(|error| Error::from_reason(format!("Cannot open WASAPI output: {error}")))
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
fn render_output<T>(output: &mut [T], state: &SharedState, mixer: &mut Mixer)
|
|
751
|
-
where
|
|
752
|
-
T: SizedSample + FromSample<f32>,
|
|
753
|
-
{
|
|
754
|
-
let channels = state.output_channels;
|
|
755
|
-
if state.shutdown.load(Ordering::Acquire) {
|
|
756
|
-
output.fill(T::from_sample(0.0));
|
|
757
|
-
return;
|
|
758
|
-
}
|
|
759
|
-
mixer.apply_commands(state);
|
|
760
|
-
let global_volume = f32::from_bits(state.global_volume_bits.load(Ordering::Acquire));
|
|
761
|
-
let global_muted = state.global_muted.load(Ordering::Acquire);
|
|
762
|
-
let mut starved = false;
|
|
763
|
-
|
|
764
|
-
for frame in output.chunks_mut(channels) {
|
|
765
|
-
let mut left = 0.0_f32;
|
|
766
|
-
let mut right = 0.0_f32;
|
|
767
|
-
for voice in mixer.voices.values_mut() {
|
|
768
|
-
if !voice.playing {
|
|
769
|
-
continue;
|
|
770
|
-
}
|
|
771
|
-
if !voice.play_announced
|
|
772
|
-
&& matches!(
|
|
773
|
-
&voice.source,
|
|
774
|
-
VoiceSource::Streaming { buffer, .. }
|
|
775
|
-
if buffer.ready.load(Ordering::Acquire)
|
|
776
|
-
)
|
|
777
|
-
{
|
|
778
|
-
voice.play_announced = true;
|
|
779
|
-
state.queue_event(voice.owner_id, "play", Some(voice.id), None, None);
|
|
780
|
-
}
|
|
781
|
-
let (voice_left, voice_right, source_finished) = match next_voice_frame(voice) {
|
|
782
|
-
VoiceFrame::Sample(left, right, finished) => (left, right, finished),
|
|
783
|
-
VoiceFrame::Pending => continue,
|
|
784
|
-
VoiceFrame::Starved => {
|
|
785
|
-
starved = true;
|
|
786
|
-
continue;
|
|
787
|
-
}
|
|
788
|
-
};
|
|
789
|
-
update_fade(voice, state);
|
|
790
|
-
let gain = if global_muted || voice.muted {
|
|
791
|
-
0.0
|
|
792
|
-
} else {
|
|
793
|
-
global_volume * voice.volume
|
|
794
|
-
};
|
|
795
|
-
left += voice_left * gain;
|
|
796
|
-
right += voice_right * gain;
|
|
797
|
-
voice.rendered_frames += 1;
|
|
798
|
-
if source_finished {
|
|
799
|
-
state.queue_event(voice.owner_id, "end", Some(voice.id), None, None);
|
|
800
|
-
if voice.looped {
|
|
801
|
-
voice.position_frames = 0.0;
|
|
802
|
-
} else {
|
|
803
|
-
voice.finished = true;
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
left = left.clamp(-1.0, 1.0);
|
|
808
|
-
right = right.clamp(-1.0, 1.0);
|
|
809
|
-
if let Some(sample) = frame.get_mut(0) {
|
|
810
|
-
*sample = T::from_sample(left);
|
|
811
|
-
}
|
|
812
|
-
if let Some(sample) = frame.get_mut(1) {
|
|
813
|
-
*sample = T::from_sample(right);
|
|
814
|
-
}
|
|
815
|
-
for sample in frame.iter_mut().skip(2) {
|
|
816
|
-
*sample = T::from_sample((left + right) * 0.5);
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
if starved {
|
|
820
|
-
state.underruns.fetch_add(1, Ordering::Relaxed);
|
|
821
|
-
}
|
|
822
|
-
state
|
|
823
|
-
.audible_frame
|
|
824
|
-
.fetch_add((output.len() / channels) as u64, Ordering::Release);
|
|
825
|
-
for voice in mixer.voices.values() {
|
|
826
|
-
update_snapshot(voice, state.sample_rate);
|
|
827
|
-
}
|
|
828
|
-
mixer.voices.retain(|id, voice| {
|
|
829
|
-
if voice.finished {
|
|
830
|
-
voice.snapshot.playing.store(false, Ordering::Release);
|
|
831
|
-
state.completed_voices.push(*id);
|
|
832
|
-
false
|
|
833
|
-
} else {
|
|
834
|
-
true
|
|
835
|
-
}
|
|
836
|
-
});
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
fn next_voice_frame(voice: &mut Voice) -> VoiceFrame {
|
|
840
|
-
match &mut voice.source {
|
|
841
|
-
VoiceSource::Loading => VoiceFrame::Pending,
|
|
842
|
-
VoiceSource::Static(samples) => {
|
|
843
|
-
let frame_count = samples.len() / CHANNELS;
|
|
844
|
-
if frame_count == 0 {
|
|
845
|
-
return VoiceFrame::Sample(0.0, 0.0, true);
|
|
846
|
-
}
|
|
847
|
-
let index = voice.position_frames.floor() as usize;
|
|
848
|
-
if index >= frame_count {
|
|
849
|
-
return VoiceFrame::Sample(0.0, 0.0, true);
|
|
850
|
-
}
|
|
851
|
-
let left = samples[index * CHANNELS];
|
|
852
|
-
let right = samples[index * CHANNELS + 1];
|
|
853
|
-
voice.position_frames += voice.playback_rate;
|
|
854
|
-
VoiceFrame::Sample(left, right, voice.position_frames >= frame_count as f64)
|
|
855
|
-
}
|
|
856
|
-
VoiceSource::Streaming {
|
|
857
|
-
buffer,
|
|
858
|
-
chunk,
|
|
859
|
-
offset,
|
|
860
|
-
} => {
|
|
861
|
-
if !buffer.ready.load(Ordering::Acquire) {
|
|
862
|
-
return VoiceFrame::Pending;
|
|
863
|
-
}
|
|
864
|
-
if chunk.is_none() {
|
|
865
|
-
*chunk = buffer.pop();
|
|
866
|
-
*offset = 0;
|
|
867
|
-
}
|
|
868
|
-
let Some(samples) = chunk.as_ref() else {
|
|
869
|
-
return if buffer.ended.load(Ordering::Acquire) {
|
|
870
|
-
VoiceFrame::Sample(0.0, 0.0, true)
|
|
871
|
-
} else {
|
|
872
|
-
VoiceFrame::Starved
|
|
873
|
-
};
|
|
874
|
-
};
|
|
875
|
-
let left = samples[*offset];
|
|
876
|
-
let right = samples[*offset + 1];
|
|
877
|
-
*offset += CHANNELS;
|
|
878
|
-
voice.position_frames += 1.0;
|
|
879
|
-
if *offset >= samples.len() {
|
|
880
|
-
*chunk = None;
|
|
881
|
-
*offset = 0;
|
|
882
|
-
}
|
|
883
|
-
VoiceFrame::Sample(left, right, false)
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
fn update_snapshot(voice: &Voice, sample_rate: u32) {
|
|
889
|
-
let media_rate = if matches!(voice.source, VoiceSource::Streaming { .. }) {
|
|
890
|
-
voice.playback_rate
|
|
891
|
-
} else {
|
|
892
|
-
1.0
|
|
893
|
-
};
|
|
894
|
-
let time = voice.offset_seconds + voice.position_frames * media_rate / sample_rate as f64;
|
|
895
|
-
voice
|
|
896
|
-
.snapshot
|
|
897
|
-
.time_bits
|
|
898
|
-
.store(time.to_bits(), Ordering::Release);
|
|
899
|
-
voice
|
|
900
|
-
.snapshot
|
|
901
|
-
.volume_bits
|
|
902
|
-
.store(voice.volume.to_bits(), Ordering::Release);
|
|
903
|
-
let remaining = match &voice.source {
|
|
904
|
-
VoiceSource::Streaming { chunk, offset, .. } => chunk
|
|
905
|
-
.as_ref()
|
|
906
|
-
.map_or(0, |samples| samples.len().saturating_sub(*offset)),
|
|
907
|
-
_ => 0,
|
|
908
|
-
};
|
|
909
|
-
voice
|
|
910
|
-
.snapshot
|
|
911
|
-
.current_chunk_samples
|
|
912
|
-
.store(remaining, Ordering::Release);
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
fn update_fade(voice: &mut Voice, state: &SharedState) {
|
|
916
|
-
let Some(fade) = &voice.fade else { return };
|
|
917
|
-
let elapsed = voice.rendered_frames.saturating_sub(fade.start_frame) + 1;
|
|
918
|
-
let progress = if fade.duration_frames == 0 {
|
|
919
|
-
1.0
|
|
920
|
-
} else {
|
|
921
|
-
(elapsed as f32 / fade.duration_frames as f32).min(1.0)
|
|
922
|
-
};
|
|
923
|
-
voice.volume = fade.from + (fade.to - fade.from) * progress;
|
|
924
|
-
if progress >= 1.0 {
|
|
925
|
-
let version = fade.version;
|
|
926
|
-
voice.fade = None;
|
|
927
|
-
state.queue_event(voice.owner_id, "fade", Some(voice.id), None, Some(version));
|
|
928
|
-
}
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
fn start_static_decode(
|
|
932
|
-
state: Arc<SharedState>,
|
|
933
|
-
options: NativeVoiceOptions,
|
|
934
|
-
preload: bool,
|
|
935
|
-
request_id: Option<u32>,
|
|
936
|
-
) {
|
|
937
|
-
thread::spawn(move || {
|
|
938
|
-
let key = cache_key(&options);
|
|
939
|
-
let cached = state
|
|
940
|
-
.cache
|
|
941
|
-
.lock()
|
|
942
|
-
.ok()
|
|
943
|
-
.and_then(|cache| cache.get(&key).cloned());
|
|
944
|
-
let decoded = match cached {
|
|
945
|
-
Some(samples) => Ok(samples),
|
|
946
|
-
None => state
|
|
947
|
-
.cache
|
|
948
|
-
.lock()
|
|
949
|
-
.ok()
|
|
950
|
-
.and_then(|cache| cache.get(&full_source_cache_key(&options.source)).cloned())
|
|
951
|
-
.map_or_else(
|
|
952
|
-
|| decode_static(&options, state.sample_rate).map(Arc::new),
|
|
953
|
-
|samples| slice_cached_samples(&samples, &options, state.sample_rate),
|
|
954
|
-
),
|
|
955
|
-
};
|
|
956
|
-
match decoded {
|
|
957
|
-
Ok(samples) => {
|
|
958
|
-
if let Ok(mut cache) = state.cache.lock() {
|
|
959
|
-
cache.insert(key, Arc::clone(&samples));
|
|
960
|
-
}
|
|
961
|
-
if preload {
|
|
962
|
-
state.queue_event(options.owner_id, "load", request_id, None, None);
|
|
963
|
-
} else {
|
|
964
|
-
state.commands.push(MixerCommand::AttachStatic {
|
|
965
|
-
id: options.id,
|
|
966
|
-
samples,
|
|
967
|
-
});
|
|
968
|
-
}
|
|
969
|
-
}
|
|
970
|
-
Err(message) => {
|
|
971
|
-
if !preload {
|
|
972
|
-
if let Ok(mut snapshots) = state.snapshots.lock() {
|
|
973
|
-
snapshots.remove(&options.id);
|
|
974
|
-
}
|
|
975
|
-
state
|
|
976
|
-
.commands
|
|
977
|
-
.push(MixerCommand::Control(NativeCommandOptions {
|
|
978
|
-
owner_id: options.owner_id,
|
|
979
|
-
command: "stop".to_string(),
|
|
980
|
-
id: Some(options.id),
|
|
981
|
-
value: None,
|
|
982
|
-
bool_value: None,
|
|
983
|
-
from: None,
|
|
984
|
-
to: None,
|
|
985
|
-
duration_ms: None,
|
|
986
|
-
fade_version: None,
|
|
987
|
-
}));
|
|
988
|
-
}
|
|
989
|
-
state.queue_event(
|
|
990
|
-
options.owner_id,
|
|
991
|
-
if preload { "loaderror" } else { "playerror" },
|
|
992
|
-
request_id.or(Some(options.id)),
|
|
993
|
-
Some(message),
|
|
994
|
-
None,
|
|
995
|
-
);
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
|
-
});
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
fn start_streaming_decode(
|
|
1002
|
-
state: Arc<SharedState>,
|
|
1003
|
-
options: NativeVoiceOptions,
|
|
1004
|
-
stream: Arc<StreamingBuffer>,
|
|
1005
|
-
) {
|
|
1006
|
-
thread::spawn(move || {
|
|
1007
|
-
if let Err(message) = decode_stream(&state, &options, &stream) {
|
|
1008
|
-
if !stream.stopped.load(Ordering::Acquire) {
|
|
1009
|
-
if let Ok(mut snapshots) = state.snapshots.lock() {
|
|
1010
|
-
snapshots.remove(&options.id);
|
|
1011
|
-
}
|
|
1012
|
-
state
|
|
1013
|
-
.commands
|
|
1014
|
-
.push(MixerCommand::Control(NativeCommandOptions {
|
|
1015
|
-
owner_id: options.owner_id,
|
|
1016
|
-
command: "stop".to_string(),
|
|
1017
|
-
id: Some(options.id),
|
|
1018
|
-
value: None,
|
|
1019
|
-
bool_value: None,
|
|
1020
|
-
from: None,
|
|
1021
|
-
to: None,
|
|
1022
|
-
duration_ms: None,
|
|
1023
|
-
fade_version: None,
|
|
1024
|
-
}));
|
|
1025
|
-
state.queue_event(
|
|
1026
|
-
options.owner_id,
|
|
1027
|
-
"playerror",
|
|
1028
|
-
Some(options.id),
|
|
1029
|
-
Some(message),
|
|
1030
|
-
None,
|
|
1031
|
-
);
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
stream.ended.store(true, Ordering::Release);
|
|
1035
|
-
});
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
fn decode_static(
|
|
1039
|
-
options: &NativeVoiceOptions,
|
|
1040
|
-
sample_rate: u32,
|
|
1041
|
-
) -> std::result::Result<Vec<f32>, String> {
|
|
1042
|
-
let mut command = ffmpeg_command(options, sample_rate, false)?;
|
|
1043
|
-
let output = command
|
|
1044
|
-
.output()
|
|
1045
|
-
.map_err(|error| format!("Cannot start FFmpeg audio decoder: {error}"))?;
|
|
1046
|
-
if !output.status.success() {
|
|
1047
|
-
return Err(redact_credentials(
|
|
1048
|
-
String::from_utf8_lossy(&output.stderr).trim(),
|
|
1049
|
-
));
|
|
1050
|
-
}
|
|
1051
|
-
bytes_to_samples(&output.stdout)
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
fn decode_stream(
|
|
1055
|
-
state: &SharedState,
|
|
1056
|
-
options: &NativeVoiceOptions,
|
|
1057
|
-
stream: &StreamingBuffer,
|
|
1058
|
-
) -> std::result::Result<(), String> {
|
|
1059
|
-
let mut child = ffmpeg_command(options, state.sample_rate, true)?
|
|
1060
|
-
.spawn()
|
|
1061
|
-
.map_err(|error| format!("Cannot start FFmpeg audio stream: {error}"))?;
|
|
1062
|
-
let mut stdout = child
|
|
1063
|
-
.stdout
|
|
1064
|
-
.take()
|
|
1065
|
-
.ok_or_else(|| "FFmpeg audio stdout is unavailable".to_string())?;
|
|
1066
|
-
let mut stderr = child
|
|
1067
|
-
.stderr
|
|
1068
|
-
.take()
|
|
1069
|
-
.ok_or_else(|| "FFmpeg audio stderr is unavailable".to_string())?;
|
|
1070
|
-
if let Ok(mut stored_child) = stream.child.lock() {
|
|
1071
|
-
*stored_child = Some(child);
|
|
1072
|
-
}
|
|
1073
|
-
let mut byte_carry = Vec::new();
|
|
1074
|
-
let mut sample_carry = Vec::new();
|
|
1075
|
-
let mut bytes = vec![0_u8; 32 * 1024];
|
|
1076
|
-
loop {
|
|
1077
|
-
if stream.stopped.load(Ordering::Acquire) || state.shutdown.load(Ordering::Acquire) {
|
|
1078
|
-
return Ok(());
|
|
1079
|
-
}
|
|
1080
|
-
let read = stdout.read(&mut bytes).map_err(|error| error.to_string())?;
|
|
1081
|
-
if read == 0 {
|
|
1082
|
-
break;
|
|
1083
|
-
}
|
|
1084
|
-
byte_carry.extend_from_slice(&bytes[..read]);
|
|
1085
|
-
let aligned = byte_carry.len() - byte_carry.len() % 4;
|
|
1086
|
-
for chunk in byte_carry[..aligned].chunks_exact(4) {
|
|
1087
|
-
sample_carry.push(f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]));
|
|
1088
|
-
}
|
|
1089
|
-
byte_carry.drain(..aligned);
|
|
1090
|
-
while sample_carry.len() >= STREAM_CHUNK_SAMPLES {
|
|
1091
|
-
let remainder = sample_carry.split_off(STREAM_CHUNK_SAMPLES);
|
|
1092
|
-
let chunk = std::mem::replace(&mut sample_carry, remainder).into_boxed_slice();
|
|
1093
|
-
if !stream.push(chunk) {
|
|
1094
|
-
return Ok(());
|
|
1095
|
-
}
|
|
1096
|
-
}
|
|
1097
|
-
}
|
|
1098
|
-
let mut child = stream
|
|
1099
|
-
.child
|
|
1100
|
-
.lock()
|
|
1101
|
-
.map_err(|_| "FFmpeg process state is poisoned".to_string())?
|
|
1102
|
-
.take()
|
|
1103
|
-
.ok_or_else(|| "FFmpeg process disappeared".to_string())?;
|
|
1104
|
-
let status = child.wait().map_err(|error| error.to_string())?;
|
|
1105
|
-
if !status.success() {
|
|
1106
|
-
let mut error = String::new();
|
|
1107
|
-
let _ = stderr.read_to_string(&mut error);
|
|
1108
|
-
return Err(redact_credentials(error.trim()));
|
|
1109
|
-
}
|
|
1110
|
-
if !sample_carry.is_empty() {
|
|
1111
|
-
sample_carry.resize(STREAM_CHUNK_SAMPLES, 0.0);
|
|
1112
|
-
if !stream.push(sample_carry.into_boxed_slice()) {
|
|
1113
|
-
return Ok(());
|
|
1114
|
-
}
|
|
1115
|
-
}
|
|
1116
|
-
if stream.produced_chunks.load(Ordering::Acquire) == 0 {
|
|
1117
|
-
return Err("Audio stream contains no decodable samples".to_string());
|
|
1118
|
-
}
|
|
1119
|
-
stream.ready.store(true, Ordering::Release);
|
|
1120
|
-
Ok(())
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
fn ffmpeg_command(
|
|
1124
|
-
options: &NativeVoiceOptions,
|
|
1125
|
-
sample_rate: u32,
|
|
1126
|
-
streaming: bool,
|
|
1127
|
-
) -> std::result::Result<Command, String> {
|
|
1128
|
-
let mut command = Command::new(&options.ffmpeg_path);
|
|
1129
|
-
command.args(["-hide_banner", "-loglevel", "error", "-nostdin"]);
|
|
1130
|
-
if options.offset_seconds > 0.0 {
|
|
1131
|
-
command.args(["-ss", &options.offset_seconds.to_string()]);
|
|
1132
|
-
}
|
|
1133
|
-
command.args(&options.input_args);
|
|
1134
|
-
command.args(["-i", &options.source, "-vn"]);
|
|
1135
|
-
if let Some(duration) = options.duration_seconds {
|
|
1136
|
-
command.args(["-t", &duration.to_string()]);
|
|
1137
|
-
}
|
|
1138
|
-
if streaming && (options.playback_rate - 1.0).abs() > f64::EPSILON {
|
|
1139
|
-
command.args(["-af", &build_atempo_filter(options.playback_rate)?]);
|
|
1140
|
-
}
|
|
1141
|
-
command.args(&options.output_args);
|
|
1142
|
-
command.args([
|
|
1143
|
-
"-ac",
|
|
1144
|
-
"2",
|
|
1145
|
-
"-ar",
|
|
1146
|
-
&sample_rate.to_string(),
|
|
1147
|
-
"-f",
|
|
1148
|
-
"f32le",
|
|
1149
|
-
"pipe:1",
|
|
1150
|
-
]);
|
|
1151
|
-
command
|
|
1152
|
-
.stdin(Stdio::null())
|
|
1153
|
-
.stdout(Stdio::piped())
|
|
1154
|
-
.stderr(Stdio::piped());
|
|
1155
|
-
#[cfg(target_os = "windows")]
|
|
1156
|
-
{
|
|
1157
|
-
use std::os::windows::process::CommandExt;
|
|
1158
|
-
command.creation_flags(0x0800_0000);
|
|
1159
|
-
}
|
|
1160
|
-
Ok(command)
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
fn build_atempo_filter(rate: f64) -> std::result::Result<String, String> {
|
|
1164
|
-
if !rate.is_finite() || rate <= 0.0 {
|
|
1165
|
-
return Err("Audio playback rate must be positive and finite".to_string());
|
|
1166
|
-
}
|
|
1167
|
-
let mut factors = Vec::new();
|
|
1168
|
-
let mut remaining = rate;
|
|
1169
|
-
while remaining < 0.5 {
|
|
1170
|
-
factors.push(0.5);
|
|
1171
|
-
remaining /= 0.5;
|
|
1172
|
-
}
|
|
1173
|
-
while remaining > 2.0 {
|
|
1174
|
-
factors.push(2.0);
|
|
1175
|
-
remaining /= 2.0;
|
|
1176
|
-
}
|
|
1177
|
-
factors.push(remaining);
|
|
1178
|
-
Ok(factors
|
|
1179
|
-
.into_iter()
|
|
1180
|
-
.map(|factor| format!("atempo={factor}"))
|
|
1181
|
-
.collect::<Vec<_>>()
|
|
1182
|
-
.join(","))
|
|
1183
|
-
}
|
|
1184
|
-
|
|
1185
|
-
fn bytes_to_samples(bytes: &[u8]) -> std::result::Result<Vec<f32>, String> {
|
|
1186
|
-
if bytes.is_empty() {
|
|
1187
|
-
return Err("Audio stream contains no decodable samples".to_string());
|
|
1188
|
-
}
|
|
1189
|
-
Ok(bytes
|
|
1190
|
-
.chunks_exact(4)
|
|
1191
|
-
.map(|chunk| f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
|
|
1192
|
-
.collect())
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
fn cache_key(options: &NativeVoiceOptions) -> String {
|
|
1196
|
-
format!(
|
|
1197
|
-
"{}\0{}\0{}",
|
|
1198
|
-
options.source,
|
|
1199
|
-
options.offset_seconds,
|
|
1200
|
-
options
|
|
1201
|
-
.duration_seconds
|
|
1202
|
-
.map_or_else(|| "end".to_string(), |value| value.to_string())
|
|
1203
|
-
)
|
|
1204
|
-
}
|
|
1205
|
-
|
|
1206
|
-
fn full_source_cache_key(source: &str) -> String {
|
|
1207
|
-
format!("{source}\0{}\0end", 0.0)
|
|
1208
|
-
}
|
|
1209
|
-
|
|
1210
|
-
fn slice_cached_samples(
|
|
1211
|
-
samples: &[f32],
|
|
1212
|
-
options: &NativeVoiceOptions,
|
|
1213
|
-
sample_rate: u32,
|
|
1214
|
-
) -> std::result::Result<Arc<Vec<f32>>, String> {
|
|
1215
|
-
let start = (options.offset_seconds * sample_rate as f64) as usize * CHANNELS;
|
|
1216
|
-
let requested = options
|
|
1217
|
-
.duration_seconds
|
|
1218
|
-
.map(|duration| (duration * sample_rate as f64) as usize * CHANNELS)
|
|
1219
|
-
.unwrap_or_else(|| samples.len().saturating_sub(start));
|
|
1220
|
-
let end = start.saturating_add(requested).min(samples.len());
|
|
1221
|
-
if start >= end {
|
|
1222
|
-
return Err("Audio sprite is outside the decoded source".to_string());
|
|
1223
|
-
}
|
|
1224
|
-
Ok(Arc::new(samples[start..end].to_vec()))
|
|
1225
|
-
}
|
|
1226
|
-
|
|
1227
|
-
fn validate_voice_options(options: &NativeVoiceOptions) -> Result<()> {
|
|
1228
|
-
if !options.offset_seconds.is_finite() || options.offset_seconds < 0.0 {
|
|
1229
|
-
return Err(Error::from_reason(
|
|
1230
|
-
"Audio offset must be non-negative and finite",
|
|
1231
|
-
));
|
|
1232
|
-
}
|
|
1233
|
-
if options
|
|
1234
|
-
.duration_seconds
|
|
1235
|
-
.is_some_and(|value| !value.is_finite() || value <= 0.0)
|
|
1236
|
-
{
|
|
1237
|
-
return Err(Error::from_reason(
|
|
1238
|
-
"Audio duration must be positive and finite",
|
|
1239
|
-
));
|
|
1240
|
-
}
|
|
1241
|
-
required_unit_value(Some(options.volume), "volume")?;
|
|
1242
|
-
if !options.playback_rate.is_finite() || options.playback_rate <= 0.0 {
|
|
1243
|
-
return Err(Error::from_reason(
|
|
1244
|
-
"Audio playback rate must be positive and finite",
|
|
1245
|
-
));
|
|
1246
|
-
}
|
|
1247
|
-
Ok(())
|
|
1248
|
-
}
|
|
1249
|
-
|
|
1250
|
-
fn required_unit_value(value: Option<f64>, name: &str) -> Result<f64> {
|
|
1251
|
-
let value = value.ok_or_else(|| Error::from_reason(format!("Missing audio {name}")))?;
|
|
1252
|
-
if !value.is_finite() || !(0.0..=1.0).contains(&value) {
|
|
1253
|
-
return Err(Error::from_reason(format!(
|
|
1254
|
-
"Audio {name} must be between 0 and 1"
|
|
1255
|
-
)));
|
|
1256
|
-
}
|
|
1257
|
-
Ok(value)
|
|
1258
|
-
}
|
|
1259
|
-
|
|
1260
|
-
fn redact_credentials(message: &str) -> String {
|
|
1261
|
-
let mut result = message.to_string();
|
|
1262
|
-
let mut start = 0;
|
|
1263
|
-
while let Some(scheme) = result[start..].find("://") {
|
|
1264
|
-
let authority_start = start + scheme + 3;
|
|
1265
|
-
let authority_end = result[authority_start..]
|
|
1266
|
-
.find(['/', ' ', '\n', '\r'])
|
|
1267
|
-
.map_or(result.len(), |offset| authority_start + offset);
|
|
1268
|
-
if let Some(at) = result[authority_start..authority_end].find('@') {
|
|
1269
|
-
let credential_end = authority_start + at;
|
|
1270
|
-
result.replace_range(authority_start..credential_end, "***:***");
|
|
1271
|
-
start = authority_start + 8;
|
|
1272
|
-
} else {
|
|
1273
|
-
start = authority_end;
|
|
1274
|
-
}
|
|
1275
|
-
}
|
|
1276
|
-
if result.trim().is_empty() {
|
|
1277
|
-
"FFmpeg audio decoder failed".to_string()
|
|
1278
|
-
} else {
|
|
1279
|
-
result
|
|
1280
|
-
}
|
|
1281
|
-
}
|
|
1282
|
-
|
|
1283
|
-
#[cfg(test)]
|
|
1284
|
-
mod tests {
|
|
1285
|
-
use super::*;
|
|
1286
|
-
|
|
1287
|
-
fn test_state() -> Arc<SharedState> {
|
|
1288
|
-
Arc::new(SharedState {
|
|
1289
|
-
sample_rate: 48_000,
|
|
1290
|
-
output_channels: 2,
|
|
1291
|
-
commands: SegQueue::new(),
|
|
1292
|
-
snapshots: Mutex::new(HashMap::new()),
|
|
1293
|
-
completed_voices: SegQueue::new(),
|
|
1294
|
-
cache: Mutex::new(HashMap::new()),
|
|
1295
|
-
events: SegQueue::new(),
|
|
1296
|
-
event_notifier: None,
|
|
1297
|
-
event_sequence: AtomicU64::new(0),
|
|
1298
|
-
audible_frame: AtomicU64::new(0),
|
|
1299
|
-
global_volume_bits: AtomicU32::new(1.0_f32.to_bits()),
|
|
1300
|
-
global_muted: AtomicBool::new(false),
|
|
1301
|
-
fade_versions: Mutex::new(HashMap::new()),
|
|
1302
|
-
underruns: AtomicU32::new(0),
|
|
1303
|
-
shutdown: AtomicBool::new(false),
|
|
1304
|
-
})
|
|
1305
|
-
}
|
|
1306
|
-
|
|
1307
|
-
fn static_voice(id: u32, samples: Vec<f32>) -> Voice {
|
|
1308
|
-
let snapshot = Arc::new(VoiceSnapshot {
|
|
1309
|
-
owner_id: 1,
|
|
1310
|
-
time_bits: AtomicU64::new(0.0_f64.to_bits()),
|
|
1311
|
-
volume_bits: AtomicU32::new(1.0_f32.to_bits()),
|
|
1312
|
-
playing: AtomicBool::new(true),
|
|
1313
|
-
stream: None,
|
|
1314
|
-
current_chunk_samples: AtomicUsize::new(0),
|
|
1315
|
-
});
|
|
1316
|
-
Voice {
|
|
1317
|
-
owner_id: 1,
|
|
1318
|
-
id,
|
|
1319
|
-
source: VoiceSource::Static(Arc::new(samples)),
|
|
1320
|
-
offset_seconds: 0.0,
|
|
1321
|
-
position_frames: 0.0,
|
|
1322
|
-
rendered_frames: 0,
|
|
1323
|
-
volume: 1.0,
|
|
1324
|
-
muted: false,
|
|
1325
|
-
looped: false,
|
|
1326
|
-
playing: true,
|
|
1327
|
-
playback_rate: 1.0,
|
|
1328
|
-
fade: None,
|
|
1329
|
-
play_announced: true,
|
|
1330
|
-
finished: false,
|
|
1331
|
-
snapshot,
|
|
1332
|
-
}
|
|
1333
|
-
}
|
|
1334
|
-
|
|
1335
|
-
#[test]
|
|
1336
|
-
fn atempo_decomposes_extreme_rates() {
|
|
1337
|
-
assert_eq!(build_atempo_filter(4.0).unwrap(), "atempo=2,atempo=2");
|
|
1338
|
-
assert_eq!(build_atempo_filter(0.25).unwrap(), "atempo=0.5,atempo=0.5");
|
|
1339
|
-
}
|
|
1340
|
-
|
|
1341
|
-
#[test]
|
|
1342
|
-
fn bounded_stream_buffer_has_two_seconds_of_capacity() {
|
|
1343
|
-
let stream = StreamingBuffer::new(48_000);
|
|
1344
|
-
assert_eq!(stream.chunks.capacity(), 47);
|
|
1345
|
-
}
|
|
1346
|
-
|
|
1347
|
-
#[test]
|
|
1348
|
-
fn overlapping_voices_are_mixed_without_clipping() {
|
|
1349
|
-
let state = test_state();
|
|
1350
|
-
let mut mixer = Mixer::default();
|
|
1351
|
-
mixer
|
|
1352
|
-
.voices
|
|
1353
|
-
.insert(1, static_voice(1, vec![0.25, 0.25, 0.0, 0.0]));
|
|
1354
|
-
mixer
|
|
1355
|
-
.voices
|
|
1356
|
-
.insert(2, static_voice(2, vec![0.5, -0.5, 0.0, 0.0]));
|
|
1357
|
-
|
|
1358
|
-
let mut output = [0.0_f32; 2];
|
|
1359
|
-
render_output(&mut output, &state, &mut mixer);
|
|
1360
|
-
assert_eq!(output, [0.75, -0.25]);
|
|
1361
|
-
}
|
|
1362
|
-
|
|
1363
|
-
#[test]
|
|
1364
|
-
fn looping_voice_emits_ordered_end_events_and_stays_active() {
|
|
1365
|
-
let state = test_state();
|
|
1366
|
-
let mut mixer = Mixer::default();
|
|
1367
|
-
let mut voice = static_voice(7, vec![0.1, 0.1]);
|
|
1368
|
-
voice.looped = true;
|
|
1369
|
-
mixer.voices.insert(7, voice);
|
|
1370
|
-
|
|
1371
|
-
render_output(&mut [0.0_f32; 6], &state, &mut mixer);
|
|
1372
|
-
assert!(mixer.voices.contains_key(&7));
|
|
1373
|
-
let mut events = Vec::new();
|
|
1374
|
-
while let Some(event) = state.events.pop() {
|
|
1375
|
-
events.push((event.target_frame, event.sequence, event.event));
|
|
1376
|
-
}
|
|
1377
|
-
assert_eq!(events.len(), 3);
|
|
1378
|
-
assert!(events.windows(2).all(|pair| pair[0] < pair[1]));
|
|
1379
|
-
}
|
|
1380
|
-
|
|
1381
|
-
#[test]
|
|
1382
|
-
fn fade_uses_rendered_sample_clock_and_fires_once() {
|
|
1383
|
-
let state = test_state();
|
|
1384
|
-
let mut mixer = Mixer::default();
|
|
1385
|
-
let mut voice = static_voice(3, vec![1.0; 16]);
|
|
1386
|
-
voice.fade = Some(Fade {
|
|
1387
|
-
from: 1.0,
|
|
1388
|
-
to: 0.0,
|
|
1389
|
-
start_frame: 0,
|
|
1390
|
-
duration_frames: 4,
|
|
1391
|
-
version: 9,
|
|
1392
|
-
});
|
|
1393
|
-
state.fade_versions.lock().unwrap().insert(3, 9);
|
|
1394
|
-
mixer.voices.insert(3, voice);
|
|
1395
|
-
|
|
1396
|
-
render_output(&mut [0.0_f32; 8], &state, &mut mixer);
|
|
1397
|
-
assert_eq!(mixer.voices.get(&3).unwrap().volume, 0.0);
|
|
1398
|
-
assert_eq!(state.events.len(), 1);
|
|
1399
|
-
}
|
|
1400
|
-
|
|
1401
|
-
#[test]
|
|
1402
|
-
fn streaming_underrun_outputs_silence_without_advancing_voice_clock() {
|
|
1403
|
-
let state = test_state();
|
|
1404
|
-
let mut mixer = Mixer::default();
|
|
1405
|
-
let stream = Arc::new(StreamingBuffer::new(48_000));
|
|
1406
|
-
stream.ready.store(true, Ordering::Release);
|
|
1407
|
-
let mut voice = static_voice(4, Vec::new());
|
|
1408
|
-
voice.snapshot = Arc::new(VoiceSnapshot {
|
|
1409
|
-
owner_id: 1,
|
|
1410
|
-
time_bits: AtomicU64::new(0.0_f64.to_bits()),
|
|
1411
|
-
volume_bits: AtomicU32::new(1.0_f32.to_bits()),
|
|
1412
|
-
playing: AtomicBool::new(true),
|
|
1413
|
-
stream: Some(Arc::clone(&stream)),
|
|
1414
|
-
current_chunk_samples: AtomicUsize::new(0),
|
|
1415
|
-
});
|
|
1416
|
-
voice.source = VoiceSource::Streaming {
|
|
1417
|
-
buffer: stream,
|
|
1418
|
-
chunk: None,
|
|
1419
|
-
offset: 0,
|
|
1420
|
-
};
|
|
1421
|
-
mixer.voices.insert(4, voice);
|
|
1422
|
-
|
|
1423
|
-
let mut output = [1.0_f32; 4];
|
|
1424
|
-
render_output(&mut output, &state, &mut mixer);
|
|
1425
|
-
assert_eq!(output, [0.0; 4]);
|
|
1426
|
-
assert_eq!(mixer.voices.get(&4).unwrap().position_frames, 0.0);
|
|
1427
|
-
assert_eq!(state.underruns.load(Ordering::Relaxed), 1);
|
|
1428
|
-
}
|
|
1429
|
-
|
|
1430
|
-
#[test]
|
|
1431
|
-
fn render_does_not_touch_js_snapshot_or_cache_locks() {
|
|
1432
|
-
let state = test_state();
|
|
1433
|
-
let mut mixer = Mixer::default();
|
|
1434
|
-
mixer
|
|
1435
|
-
.voices
|
|
1436
|
-
.insert(1, static_voice(1, vec![0.25, 0.25, 0.0, 0.0]));
|
|
1437
|
-
let _snapshots = state.snapshots.lock().unwrap();
|
|
1438
|
-
let _cache = state.cache.lock().unwrap();
|
|
1439
|
-
let mut output = [0.0_f32; 2];
|
|
1440
|
-
render_output(&mut output, &state, &mut mixer);
|
|
1441
|
-
assert_eq!(output, [0.25, 0.25]);
|
|
1442
|
-
}
|
|
1443
|
-
|
|
1444
|
-
#[test]
|
|
1445
|
-
fn sustained_streaming_stays_bounded_without_starvation() {
|
|
1446
|
-
let state = test_state();
|
|
1447
|
-
let stream = Arc::new(StreamingBuffer::new(48_000));
|
|
1448
|
-
let producer_stream = Arc::clone(&stream);
|
|
1449
|
-
let producer = thread::spawn(move || {
|
|
1450
|
-
for _ in 0..(30 * 48_000 / STREAM_CHUNK_FRAMES) {
|
|
1451
|
-
assert!(producer_stream.push(vec![0.1; STREAM_CHUNK_SAMPLES].into_boxed_slice()));
|
|
1452
|
-
}
|
|
1453
|
-
producer_stream.ended.store(true, Ordering::Release);
|
|
1454
|
-
});
|
|
1455
|
-
while stream.chunks.len() < STREAM_START_CHUNKS {
|
|
1456
|
-
thread::yield_now();
|
|
1457
|
-
}
|
|
1458
|
-
let mut voice = static_voice(5, Vec::new());
|
|
1459
|
-
voice.snapshot = Arc::new(VoiceSnapshot {
|
|
1460
|
-
owner_id: 1,
|
|
1461
|
-
time_bits: AtomicU64::new(0.0_f64.to_bits()),
|
|
1462
|
-
volume_bits: AtomicU32::new(1.0_f32.to_bits()),
|
|
1463
|
-
playing: AtomicBool::new(true),
|
|
1464
|
-
stream: Some(Arc::clone(&stream)),
|
|
1465
|
-
current_chunk_samples: AtomicUsize::new(0),
|
|
1466
|
-
});
|
|
1467
|
-
voice.source = VoiceSource::Streaming {
|
|
1468
|
-
buffer: Arc::clone(&stream),
|
|
1469
|
-
chunk: None,
|
|
1470
|
-
offset: 0,
|
|
1471
|
-
};
|
|
1472
|
-
voice.play_announced = false;
|
|
1473
|
-
let mut mixer = Mixer::default();
|
|
1474
|
-
mixer.voices.insert(5, voice);
|
|
1475
|
-
let mut output = vec![0.0_f32; STREAM_CHUNK_SAMPLES];
|
|
1476
|
-
for _ in 0..(30 * 48_000 / STREAM_CHUNK_FRAMES) {
|
|
1477
|
-
render_output(&mut output, &state, &mut mixer);
|
|
1478
|
-
assert!(stream.chunks.len() <= stream.chunks.capacity());
|
|
1479
|
-
}
|
|
1480
|
-
producer.join().unwrap();
|
|
1481
|
-
assert_eq!(state.underruns.load(Ordering::Relaxed), 0);
|
|
1482
|
-
}
|
|
1483
|
-
|
|
1484
|
-
#[test]
|
|
1485
|
-
fn streaming_teardown_wakes_backpressure_waiters() {
|
|
1486
|
-
let stream = StreamingBuffer::new(48_000);
|
|
1487
|
-
stream.stop();
|
|
1488
|
-
assert!(stream.stopped.load(Ordering::Acquire));
|
|
1489
|
-
}
|
|
1490
|
-
|
|
1491
|
-
#[test]
|
|
1492
|
-
fn credentials_are_redacted_from_decoder_errors() {
|
|
1493
|
-
assert_eq!(
|
|
1494
|
-
redact_credentials("https://user:secret@example.test/audio.mp3 failed"),
|
|
1495
|
-
"https://***:***@example.test/audio.mp3 failed"
|
|
1496
|
-
);
|
|
1497
|
-
}
|
|
1498
|
-
|
|
1499
|
-
#[test]
|
|
1500
|
-
fn preloaded_source_is_sliced_for_sprite_playback() {
|
|
1501
|
-
let options = NativeVoiceOptions {
|
|
1502
|
-
owner_id: 1,
|
|
1503
|
-
id: 2,
|
|
1504
|
-
source: "atlas.wav".to_string(),
|
|
1505
|
-
ffmpeg_path: "ffmpeg".to_string(),
|
|
1506
|
-
offset_seconds: 0.25,
|
|
1507
|
-
duration_seconds: Some(0.5),
|
|
1508
|
-
volume: 1.0,
|
|
1509
|
-
muted: false,
|
|
1510
|
-
loop_: false,
|
|
1511
|
-
streaming: false,
|
|
1512
|
-
playback_rate: 1.0,
|
|
1513
|
-
input_args: Vec::new(),
|
|
1514
|
-
output_args: Vec::new(),
|
|
1515
|
-
};
|
|
1516
|
-
let samples = vec![0.0; 48_000 * CHANNELS];
|
|
1517
|
-
let sliced = slice_cached_samples(&samples, &options, 48_000).unwrap();
|
|
1518
|
-
assert_eq!(sliced.len(), 24_000 * CHANNELS);
|
|
1519
|
-
}
|
|
1520
|
-
}
|
|
5
|
+
pub use audio_impl::*;
|