Skip to content

Commit d12d007

Browse files
committed
fix(audio): credit all but one ticker period of a queue stall
Withholding the whole 100 ms threshold took that much grace from a live source on every block, so one 150-250 ms behind could be written off and lose its queued audio. One 10 ms period is enough to bound a sluggish queue.
1 parent 549f13d commit d12d007

2 files changed

Lines changed: 40 additions & 5 deletions

File tree

‎electron/native/screencapturekit/Sources/OpenScreenCaptureCore/AudioTrackMixer.swift‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -217,16 +217,19 @@ public final class AudioTrackMixer {
217217
/// the ticker allows moves each source's last delivery forward by the gap, which measures
218218
/// the grace against the time the queue was actually running.
219219
///
220-
/// Every pass still counts `queueStallFrames` as running time, so a queue that is merely
220+
/// Every pass still counts one ticker period as running time, so a queue that is merely
221221
/// sluggish — passes always a little over the threshold apart — cannot keep a dead source
222-
/// inside the grace forever: it stalls after a few passes and the track keeps moving.
222+
/// inside the grace forever: it stalls after at most `emissionGraceFrames / chunkFrames`
223+
/// passes and the track keeps moving. One period and not the whole threshold, because
224+
/// whatever is not credited is grace taken from a live source on every block.
223225
private func discountQueueStall(until now: CMTime) {
224226
guard let anchor, now.isValid, now.isNumeric else {
225227
return
226228
}
227229
let nowFrame = frameIndex(of: now, from: anchor)
228230
if let lastPassFrame, nowFrame - lastPassFrame > Int64(MixFormat.queueStallFrames) {
229-
let stall = nowFrame - lastPassFrame - Int64(MixFormat.queueStallFrames)
231+
// The ticker period is the chunk size: 10 ms.
232+
let stall = nowFrame - lastPassFrame - Int64(MixFormat.chunkFrames)
230233
for index in sources.indices {
231234
sources[index].lastDeliveryFrame += stall
232235
}

‎electron/native/screencapturekit/Tests/OpenScreenCaptureCoreTests/AudioTrackMixerTests.swift‎

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -419,7 +419,9 @@ final class AudioTrackMixerTests: XCTestCase {
419419
makeSourceBuffer(burst(seconds: 0.15, left: 0, right: 0.5), at: clock.now, nonInterleaved: false),
420420
from: .microphone
421421
)
422-
for _ in 0..<20 {
422+
// 40 passes, 6 s: each credits 10 ms of running time, so the 250 ms grace runs out by
423+
// the 26th and the microphone is written off with seconds of take still to go.
424+
for _ in 0..<40 {
423425
mixer.ingest(
424426
makeSourceBuffer(burst(seconds: 0.15, left: 0.5, right: 0), at: clock.now, nonInterleaved: true),
425427
from: .system
@@ -429,7 +431,37 @@ final class AudioTrackMixerTests: XCTestCase {
429431
}
430432

431433
// Before `finish`: the track has to have kept moving without the microphone.
432-
XCTAssertGreaterThan(seconds(ofFrames: mixedFrames(sink)), 2.0)
434+
XCTAssertGreaterThan(seconds(ofFrames: mixedFrames(sink)), 5.0)
435+
}
436+
437+
/// A block must not eat into the grace of a source that is late but alive: here 200 ms
438+
/// behind, within the 250 ms grace, when the queue blocks for a second.
439+
func testABlockedQueueKeepsTheGraceOfALateSource() {
440+
let sink = RecordingSink()
441+
let clock = TestClock(.zero)
442+
let mixer = makeMixer(sink: sink, clock: clock, includesMicrophone: false)
443+
mixer.beginTimeline(at: clock.now)
444+
445+
mixer.ingest(
446+
makeSourceBuffer(burst(seconds: 0.2, left: 0.5, right: 0.5), at: .zero, nonInterleaved: true),
447+
from: .system
448+
)
449+
runTicker(mixer, clock, seconds: 0.2)
450+
clock.advance(seconds: 1)
451+
mixer.tick()
452+
mixer.ingest(
453+
makeSourceBuffer(
454+
burst(seconds: 1, left: 0.5, right: 0.5),
455+
at: CMTime(seconds: 0.2, preferredTimescale: 48_000),
456+
nonInterleaved: true
457+
),
458+
from: .system
459+
)
460+
runTicker(mixer, clock, seconds: 0.1)
461+
mixer.finish(atSourceTime: clock.now)
462+
463+
XCTAssertEqual(mixer.deliveryReport(for: .system).droppedSeconds, 0, accuracy: 0.0001)
464+
XCTAssertGreaterThan(peak(mixedFrames(sink), from: 0.2, to: 1.2), audibleThreshold)
433465
}
434466

435467
private func assertBlockedQueueKeepsQueuedAudio(tickFirst: Bool) {

0 commit comments

Comments
 (0)