Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -216,15 +216,22 @@ public final class AudioTrackMixer {
/// whole time; it is the mixer that was not listening. So a pass that comes much later than
/// the ticker allows moves each source's last delivery forward by the gap, which measures
/// the grace against the time the queue was actually running.
///
/// Every pass still counts one ticker period as running time, so a queue that is merely
/// sluggish — passes always a little over the threshold apart — cannot keep a dead source
/// inside the grace forever: it stalls after at most `emissionGraceFrames / chunkFrames`
/// passes and the track keeps moving. One period and not the whole threshold, because
/// whatever is not credited is grace taken from a live source on every block.
private func discountQueueStall(until now: CMTime) {
guard let anchor, now.isValid, now.isNumeric else {
return
}
let nowFrame = frameIndex(of: now, from: anchor)
if let lastPassFrame, nowFrame - lastPassFrame > Int64(MixFormat.queueStallFrames) {
let gap = nowFrame - lastPassFrame
// The ticker period is the chunk size: 10 ms.
let stall = nowFrame - lastPassFrame - Int64(MixFormat.chunkFrames)
for index in sources.indices {
sources[index].lastDeliveryFrame += gap
sources[index].lastDeliveryFrame += stall
}
}
lastPassFrame = nowFrame
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,72 @@ final class AudioTrackMixerTests: XCTestCase {
/// clock alone, both sources would look a second quiet, the cursor would run to now in
/// silence, and the queued second would then arrive behind it and be dropped.
func testABlockedQueueDoesNotTurnQueuedAudioIntoSilence() {
assertBlockedQueueKeepsQueuedAudio(tickFirst: true)
}

/// The same stall with a queued buffer first in line instead of the ticker.
func testABlockedQueueDoesNotTurnQueuedAudioIntoSilenceWhenABufferIsFirst() {
assertBlockedQueueKeepsQueuedAudio(tickFirst: false)
}

/// A sluggish queue must not keep a dead source alive: passes 150 ms apart, just over the
/// stall threshold, still let a microphone that stopped be written off mid-take.
func testASluggishQueueStillWritesOffADeadSource() {
let sink = RecordingSink()
let clock = TestClock(.zero)
let mixer = makeMixer(sink: sink, clock: clock, includesMicrophone: true, microphoneGain: 1)
mixer.beginTimeline(at: clock.now)

mixer.ingest(
makeSourceBuffer(burst(seconds: 0.15, left: 0, right: 0.5), at: clock.now, nonInterleaved: false),
from: .microphone
)
// 40 passes, 6 s: each credits 10 ms of running time, so the 250 ms grace runs out by
// the 26th and the microphone is written off with seconds of take still to go.
for _ in 0..<40 {
mixer.ingest(
makeSourceBuffer(burst(seconds: 0.15, left: 0.5, right: 0), at: clock.now, nonInterleaved: true),
from: .system
)
clock.advance(seconds: 0.15)
mixer.tick()
}

// Before `finish`: the track has to have kept moving without the microphone.
XCTAssertGreaterThan(seconds(ofFrames: mixedFrames(sink)), 5.0)
}

/// A block must not eat into the grace of a source that is late but alive: here 200 ms
/// behind, within the 250 ms grace, when the queue blocks for a second.
func testABlockedQueueKeepsTheGraceOfALateSource() {
let sink = RecordingSink()
let clock = TestClock(.zero)
let mixer = makeMixer(sink: sink, clock: clock, includesMicrophone: false)
mixer.beginTimeline(at: clock.now)

mixer.ingest(
makeSourceBuffer(burst(seconds: 0.2, left: 0.5, right: 0.5), at: .zero, nonInterleaved: true),
from: .system
)
runTicker(mixer, clock, seconds: 0.2)
clock.advance(seconds: 1)
mixer.tick()
mixer.ingest(
makeSourceBuffer(
burst(seconds: 1, left: 0.5, right: 0.5),
at: CMTime(seconds: 0.2, preferredTimescale: 48_000),
nonInterleaved: true
),
from: .system
)
runTicker(mixer, clock, seconds: 0.1)
mixer.finish(atSourceTime: clock.now)

XCTAssertEqual(mixer.deliveryReport(for: .system).droppedSeconds, 0, accuracy: 0.0001)
XCTAssertGreaterThan(peak(mixedFrames(sink), from: 0.2, to: 1.2), audibleThreshold)
}

private func assertBlockedQueueKeepsQueuedAudio(tickFirst: Bool) {
let sink = RecordingSink()
let clock = TestClock(.zero)
let mixer = makeMixer(sink: sink, clock: clock, includesMicrophone: true, microphoneGain: 1)
Expand All @@ -424,7 +490,9 @@ final class AudioTrackMixerTests: XCTestCase {

// The queue is blocked for a second: no tick, no buffer, while the clock runs on.
clock.advance(seconds: 1)
mixer.tick()
if tickFirst {
mixer.tick()
}
for _ in 0..<100 {
deliverBoth(seconds: 0.01)
}
Expand Down
Loading