@@ -237,17 +237,15 @@ final class AudioTrackMixerTests: XCTestCase {
237237 makeSourceBuffer ( burst ( seconds : 0.2 , left : 0 , right : 0.5 ) , at : clock . now , nonInterleaved : false ) ,
238238 from : . microphone
239239 )
240- clock . advance ( seconds : 0.2 )
241- mixer . tick ( )
240+ runTicker ( mixer , clock , seconds : 0.2 )
242241
243242 for _ in 0 ..< 10 {
244243 // Each buffer covers the 200 ms about to elapse, the way a live source delivers.
245244 mixer . ingest (
246245 makeSourceBuffer ( burst ( seconds : 0.2 , left : 0.5 , right : 0 ) , at : clock . now , nonInterleaved : true ) ,
247246 from : . system
248247 )
249- clock . advance ( seconds : 0.2 )
250- mixer . tick ( )
248+ runTicker ( mixer , clock , seconds : 0.2 )
251249 }
252250 clock . advance ( seconds : 0.2 )
253251 mixer . finish ( atSourceTime : clock . now )
@@ -370,9 +368,9 @@ final class AudioTrackMixerTests: XCTestCase {
370368 makeSourceBuffer ( burst ( seconds : 0.5 , left : 0.5 , right : 0.5 ) , at : . zero , nonInterleaved : true ) ,
371369 from : . system
372370 )
373- // Long enough past the grace that the source is written off and the cursor runs on.
374- clock . advance ( seconds : 2 )
375- mixer . tick ( )
371+ // Long enough past the grace that the source is written off and the cursor runs on —
372+ // with the queue running, so this is a quiet source and not a blocked queue.
373+ runTicker ( mixer , clock , seconds : 2 )
376374
377375 // …and only now does it hand over the span it went quiet for, which is already behind
378376 // the cursor and cannot be placed.
@@ -393,6 +391,67 @@ final class AudioTrackMixerTests: XCTestCase {
393391 XCTAssertEqual ( report . trimmedSeconds , 0 , accuracy : 0.011 )
394392 }
395393
394+ /// A blocked sample queue is not a silent source (#1117).
395+ ///
396+ /// Both sources keep delivering while the queue is held for a second — by the writer, or by
397+ /// anything else on it — so their buffers for that second wait behind the block. When the
398+ /// queue frees up, the ticker gets there first, which is the worst order: measured on the
399+ /// clock alone, both sources would look a second quiet, the cursor would run to now in
400+ /// silence, and the queued second would then arrive behind it and be dropped.
401+ func testABlockedQueueDoesNotTurnQueuedAudioIntoSilence ( ) {
402+ let sink = RecordingSink ( )
403+ let clock = TestClock ( . zero )
404+ let mixer = makeMixer ( sink : sink , clock : clock , includesMicrophone : true , microphoneGain : 1 )
405+ mixer . beginTimeline ( at : clock . now )
406+
407+ var delivered = CMTime . zero
408+ func deliverBoth ( seconds : Double ) {
409+ mixer . ingest (
410+ makeSourceBuffer ( burst ( seconds : seconds , left : 0.5 , right : 0 ) , at : delivered , nonInterleaved : true ) ,
411+ from : . system
412+ )
413+ mixer . ingest (
414+ makeSourceBuffer ( burst ( seconds : seconds , left : 0 , right : 0.5 ) , at : delivered , nonInterleaved : false ) ,
415+ from : . microphone
416+ )
417+ delivered = CMTimeAdd ( delivered , CMTime ( seconds : seconds , preferredTimescale : 48_000 ) )
418+ }
419+
420+ for _ in 0 ..< 50 {
421+ deliverBoth ( seconds : 0.01 )
422+ runTicker ( mixer , clock , seconds : 0.01 )
423+ }
424+
425+ // The queue is blocked for a second: no tick, no buffer, while the clock runs on.
426+ clock . advance ( seconds : 1 )
427+ mixer . tick ( )
428+ for _ in 0 ..< 100 {
429+ deliverBoth ( seconds : 0.01 )
430+ }
431+
432+ for _ in 0 ..< 50 {
433+ deliverBoth ( seconds : 0.01 )
434+ runTicker ( mixer , clock , seconds : 0.01 )
435+ }
436+ mixer . finish ( atSourceTime : clock . now )
437+
438+ let frames = mixedFrames ( sink )
439+ assertContiguous ( sink )
440+ XCTAssertEqual ( seconds ( ofFrames : frames ) , 2.0 , accuracy : 0.011 )
441+ XCTAssertEqual ( mixer . deliveryReport ( for : . system ) . droppedSeconds , 0 , accuracy : 0.0001 )
442+ XCTAssertEqual ( mixer . deliveryReport ( for : . microphone ) . droppedSeconds , 0 , accuracy : 0.0001 )
443+ for step in 0 ..< 20 {
444+ let from = Double ( step ) * 0.1
445+ for channel in 0 ..< channelCount {
446+ XCTAssertGreaterThan (
447+ peak ( frames , from : from , to : from + 0.1 , channel : channel ) ,
448+ audibleThreshold ,
449+ " silence on channel \( channel ) at \( from ) s means queued audio was dropped "
450+ )
451+ }
452+ }
453+ }
454+
396455 // MARK: - Pause
397456
398457 /// A pause stops the clock instead of resetting it. `ScreenCaptureRecorder.timelineNow()`
@@ -763,6 +822,14 @@ final class AudioTrackMixerTests: XCTestCase {
763822 }
764823 }
765824
825+ /// Moves the clock the way the recorder's 10 ms ticker sees it, with the queue running.
826+ private func runTicker ( _ mixer : AudioTrackMixer , _ clock : TestClock , seconds : Double ) {
827+ for _ in 0 ..< Int ( ( seconds * 100 ) . rounded ( ) ) {
828+ clock . advance ( seconds : 0.01 )
829+ mixer . tick ( )
830+ }
831+ }
832+
766833 private func peak ( _ samples : [ Int16 ] , from : Double , to : Double , channel : Int ? = nil ) -> Int16 {
767834 let firstFrame = Int ( from * Double ( sampleRate ) )
768835 let lastFrame = min ( Int ( to * Double ( sampleRate ) ) , samples . count / channelCount )
0 commit comments