Skip to content

Commit 79275c2

Browse files
committed
fix(jack): stale port latency when draining
1 parent 6619838 commit 79275c2

2 files changed

Lines changed: 55 additions & 43 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6565
- **iOS**: Fix timestamps and `buffer_size()` being off when the stream sample rate differs from the hardware rate.
6666
- **JACK**: Channel enumeration is capped at the physical system port count again.
6767
- **JACK**: Streams no longer panic when the server delivers a larger period than the negotiated buffer size.
68+
- **JACK**: `stop()` now drains by the delay last reported to the `playback` timestamp, instead of a stale port latency query.
6869
- **PipeWire**: Fix an empty chunk being emitted when a cycle requests no frames.
6970
- **PipeWire**: Fix capture reading from the wrong offset in the buffer on some devices.
7071
- **PipeWire**: Building a stream without a timeout now waits indefinitely instead of failing after two seconds.

‎src/host/jack/stream.rs‎

Lines changed: 54 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
use std::sync::{
22
Arc, Mutex,
3-
atomic::{AtomicBool, AtomicU8, Ordering},
3+
atomic::{AtomicBool, AtomicU8, AtomicU64, Ordering},
44
};
55

66
use super::JACK_SAMPLE_FORMAT;
77
use crate::host::try_emit_error;
88
use crate::{
99
CallbackInfo, ChannelCount, Data, DuplexCallbackInfo, Error, ErrorKind, FrameCount, ResultExt,
1010
Sample, SampleRate, StreamInstant, StreamTimestamp,
11-
host::{ErrorCallbackArc, emit_error, frames_to_duration},
11+
host::{ErrorCallbackArc, emit_error, frames_to_duration, wait_for_drain},
1212
traits::StreamTrait,
1313
};
1414

@@ -40,6 +40,8 @@ pub struct Stream {
4040
// Port names are stored in order to connect them to other ports in jack automatically
4141
input_port_names: Box<[String]>,
4242
output_port_names: Box<[String]>,
43+
// Written by the process thread every cycle. Stays zero on capture: no drain.
44+
playback_delay_nanos: Arc<AtomicU64>,
4345
}
4446

4547
impl Stream {
@@ -191,6 +193,7 @@ where
191193
let playback_state = Arc::new(AtomicU8::new(StreamState::Starting as u8));
192194
let pending_xrun = Arc::new(AtomicBool::new(false));
193195
let error_callback_ptr: ErrorCallbackArc = Arc::new(Mutex::new(error_callback));
196+
let playback_delay_nanos = Arc::new(AtomicU64::new(0));
194197

195198
let process_handler = LocalProcessHandler::new(
196199
out_ports,
@@ -201,6 +204,7 @@ where
201204
playback_state.clone(),
202205
pending_xrun.clone(),
203206
error_callback_ptr.clone(),
207+
playback_delay_nanos.clone(),
204208
);
205209

206210
let notification_handler = JackNotificationHandler::new(
@@ -220,6 +224,7 @@ where
220224
async_client,
221225
input_port_names: input_port_names.into_boxed_slice(),
222226
output_port_names: output_port_names.into_boxed_slice(),
227+
playback_delay_nanos,
223228
})
224229
}
225230

@@ -252,24 +257,11 @@ impl StreamTrait for Stream {
252257
}
253258

254259
fn stop(&self, timeout: Option<std::time::Duration>) -> Result<(), Error> {
255-
StreamState::Paused.store(&self.playback_state, Ordering::Relaxed);
256-
257-
let is_output = !self.output_port_names.is_empty();
258-
if is_output && timeout != Some(std::time::Duration::ZERO) {
259-
let client = self.async_client.as_client();
260-
let ports: Vec<_> = self
261-
.output_port_names
262-
.iter()
263-
.filter_map(|name| client.port_by_name(name))
264-
.collect();
265-
let latency_frames = hardware_latency_frames(&ports, jack::LatencyType::Playback)
266-
.unwrap_or(client.buffer_size() as FrameCount);
267-
let buffered = frames_to_duration(latency_frames, client.sample_rate() as SampleRate);
268-
let wait = timeout.map_or(buffered, |t| buffered.min(t));
269-
if !wait.is_zero() {
270-
std::thread::sleep(wait);
271-
}
272-
}
260+
self.pause()?;
261+
wait_for_drain(
262+
std::time::Duration::from_nanos(self.playback_delay_nanos.load(Ordering::Relaxed)),
263+
timeout,
264+
);
273265
Ok(())
274266
}
275267

@@ -307,6 +299,7 @@ struct LocalProcessHandler {
307299
playback_state: Arc<AtomicU8>,
308300
pending_xrun: Arc<AtomicBool>,
309301
error_callback: ErrorCallbackArc,
302+
playback_delay_nanos: Arc<AtomicU64>,
310303
oversized_reported: bool,
311304
#[cfg(feature = "realtime")]
312305
rt_checked: bool,
@@ -323,6 +316,7 @@ impl LocalProcessHandler {
323316
playback_state: Arc<AtomicU8>,
324317
pending_xrun: Arc<AtomicBool>,
325318
error_callback: ErrorCallbackArc,
319+
playback_delay_nanos: Arc<AtomicU64>,
326320
) -> Self {
327321
let temp_input_buffer = vec![f32::EQUILIBRIUM; in_ports.len() * buffer_size];
328322
let temp_output_buffer = vec![f32::EQUILIBRIUM; out_ports.len() * buffer_size];
@@ -338,6 +332,7 @@ impl LocalProcessHandler {
338332
playback_state,
339333
pending_xrun,
340334
error_callback,
335+
playback_delay_nanos,
341336
oversized_reported: false,
342337
#[cfg(feature = "realtime")]
343338
rt_checked: false,
@@ -542,13 +537,15 @@ impl jack::ProcessHandler for LocalProcessHandler {
542537

543538
let capture =
544539
capture_instant(&self.in_ports, start_cycle_instant, self.sample_rate);
545-
let playback = playback_instant(
546-
&self.out_ports,
547-
start_cycle_instant,
548-
next_usecs_opt,
549-
current_frame_count as FrameCount,
550-
self.sample_rate,
551-
);
540+
let playback = start_cycle_instant
541+
+ publish_playback_delay(
542+
&self.out_ports,
543+
start_cycle_instant,
544+
next_usecs_opt,
545+
current_frame_count as FrameCount,
546+
self.sample_rate,
547+
&self.playback_delay_nanos,
548+
);
552549
let info = DuplexCallbackInfo::new(
553550
CallbackInfo {
554551
timestamp: StreamTimestamp {
@@ -596,13 +593,15 @@ impl jack::ProcessHandler for LocalProcessHandler {
596593
);
597594
let timestamp = StreamTimestamp {
598595
callback: start_callback_instant,
599-
device: playback_instant(
600-
&self.out_ports,
601-
start_cycle_instant,
602-
next_usecs_opt,
603-
current_frame_count as FrameCount,
604-
self.sample_rate,
605-
),
596+
device: start_cycle_instant
597+
+ publish_playback_delay(
598+
&self.out_ports,
599+
start_cycle_instant,
600+
next_usecs_opt,
601+
current_frame_count as FrameCount,
602+
self.sample_rate,
603+
&self.playback_delay_nanos,
604+
),
606605
};
607606
let info = CallbackInfo { timestamp, xrun };
608607
output_callback(&mut data, &info);
@@ -674,27 +673,39 @@ fn capture_instant(
674673
.unwrap_or(StreamInstant::ZERO)
675674
}
676675

677-
/// When the first frame written this cycle reaches the DAC, derived from JACK's port-to-hardware
678-
/// playback latency, or the cycle's hardware deadline if JACK reports no latency.
676+
/// How long from the cycle start until the first frame written this cycle reaches the DAC, derived
677+
/// from JACK's port-to-hardware playback latency, or the cycle's hardware deadline if JACK reports
678+
/// no latency.
679+
///
680+
/// Also publishes it to `published` for `stop()`, which cannot resolve the `next_usecs` fallback
681+
/// off the process thread and must drain by exactly what the timestamps promised.
679682
#[inline]
680-
fn playback_instant(
683+
fn publish_playback_delay(
681684
out_ports: &[jack::Port<jack::AudioOut>],
682685
start_cycle_instant: StreamInstant,
683686
next_usecs_opt: Option<u64>,
684687
current_frame_count: FrameCount,
685688
sample_rate: SampleRate,
686-
) -> StreamInstant {
687-
match hardware_latency_frames(out_ports, jack::LatencyType::Playback) {
689+
published: &AtomicU64,
690+
) -> std::time::Duration {
691+
let delay = match hardware_latency_frames(out_ports, jack::LatencyType::Playback) {
688692
// Prefer JACK's port-to-hardware latency, measured from the cycle start.
689-
Some(frames) => start_cycle_instant + frames_to_duration(frames, sample_rate),
693+
Some(frames) => frames_to_duration(frames, sample_rate),
690694
// When no latency is reported, fall back to next_usecs, the hardware deadline for this
691695
// cycle.
692696
None => match next_usecs_opt {
693-
Some(next_usecs) => micros_to_stream_instant(next_usecs),
697+
Some(next_usecs) => micros_to_stream_instant(next_usecs)
698+
.checked_duration_since(start_cycle_instant)
699+
.unwrap_or_default(),
694700
// Fallback to one buffer ahead if that is unavailable too.
695-
None => start_cycle_instant + frames_to_duration(current_frame_count, sample_rate),
701+
None => frames_to_duration(current_frame_count, sample_rate),
696702
},
697-
}
703+
};
704+
published.store(
705+
u64::try_from(delay.as_nanos()).unwrap_or(u64::MAX),
706+
Ordering::Relaxed,
707+
);
708+
delay
698709
}
699710

700711
/// Receives notifications from the JACK server on JACK's notification thread (single-threaded).

0 commit comments

Comments
 (0)