import Foundation import XCTest @testable import CallsKit private func reading(_ rms: Float, buffers: Int = 10) -> CaptureLevel.Reading { CaptureLevel.Reading(rms: rms, peak: rms, buffers: buffers, at: 0) } private func dbfs(_ db: Float) -> Float { pow(10, db / 20) } /// The microphone verdict: working on the processed level, silent on the /// level before the suppressor, over a five-second window. final class MicJudgeTests: XCTestCase { func testSpeechAfterTheSuppressorIsWorking() { var judge = MicJudge() judge.start(at: 0) XCTAssertEqual(judge.observe(input: reading(0), output: reading(0), at: 0.1), .listening) XCTAssertEqual( judge.observe(input: reading(dbfs(-30)), output: reading(dbfs(-40)), at: 1), .working) // Stays working between words. XCTAssertEqual( judge.observe(input: reading(dbfs(-60)), output: reading(dbfs(-80)), at: 3), .working) } func testFiveSecondsOfNothingBeforeTheSuppressorIsSilent() { var judge = MicJudge() judge.start(at: 0) XCTAssertEqual(judge.observe(input: reading(dbfs(-100)), output: reading(0), at: 4.9), .listening) XCTAssertEqual(judge.observe(input: reading(dbfs(-100)), output: reading(0), at: 5), .silent) XCTAssertEqual( MicJudge.sentence(.silent, device: "Scarlett 18i8 USB"), "Sound is arriving from Scarlett 18i8 USB, but it is silent.") XCTAssertNotNil(MicJudge.advice(.silent)) } /// Nothing arriving at all is not the same as silence arriving, and is /// said differently: up to 0.3.7 both were "We can't hear anything", /// and Paul's AirPods could not tell which (2026-09-26). func testNoBuffersAtAllIsNoAudioNotSilent() { var judge = MicJudge() judge.start(at: 0) XCTAssertEqual(judge.observe(input: reading(0, buffers: 0), output: reading(0, buffers: 0), at: 4.9), .listening) XCTAssertEqual(judge.observe(input: reading(0, buffers: 0), output: reading(0, buffers: 0), at: 6), .noAudio) XCTAssertEqual( MicJudge.sentence(.noAudio, device: "Paul’s AirPods Pro (Bluetooth, 24 kHz)"), "No sound is arriving from Paul’s AirPods Pro (Bluetooth, 24 kHz). Calls asked for it and nothing came back.") XCTAssertNotNil(MicJudge.advice(.noAudio)) // Buffers of silence arriving later are the device's silence. XCTAssertEqual(judge.observe(input: reading(0), output: reading(0), at: 12), .silent) } /// A quiet room DeepFilterNet has taken down 30 dB: dead after the /// suppressor, alive before it. Not "can't hear anything". func testAQuietRoomPulledDownByTheSuppressorIsNotSilent() { var judge = MicJudge() judge.start(at: 0) for t in stride(from: 0.1, through: 8, by: 0.1) { judge.observe(input: reading(dbfs(-65)), output: reading(dbfs(-95)), at: t) } XCTAssertEqual(judge.verdict, .hearing) XCTAssertTrue(MicJudge.sentence(.hearing, device: "MacBook Pro Microphone").contains("MacBook Pro Microphone")) } func testSpeechBelowTheLineIsHearingNotWorking() { var judge = MicJudge() judge.start(at: 0) XCTAssertEqual(judge.observe(input: reading(dbfs(-50)), output: reading(dbfs(-50)), at: 1), .hearing) } func testWorkingThatGoesDeadIsSaidAndComesBack() { var judge = MicJudge() judge.start(at: 0) judge.observe(input: reading(dbfs(-20)), output: reading(dbfs(-20)), at: 1) XCTAssertEqual(judge.observe(input: reading(0), output: reading(0), at: 5.9), .working) XCTAssertEqual(judge.observe(input: reading(0), output: reading(0), at: 6), .silent) XCTAssertEqual(judge.observe(input: reading(dbfs(-60)), output: reading(dbfs(-70)), at: 7), .hearing) XCTAssertEqual(judge.observe(input: reading(dbfs(-20)), output: reading(dbfs(-20)), at: 8), .working) } /// In a call alone, WebRTC has not opened the microphone: no audio is /// not the device's fault and is not said to be. func testACallThatHasNotStartedTheMicrophoneIsNotCalledSilent() { var judge = MicJudge() judge.start(at: 0) XCTAssertEqual( judge.observe(input: reading(0, buffers: 0), output: reading(0, buffers: 0), at: 6, captureExpected: false), .notStarted) // Buffers of silence, though, are the device. XCTAssertEqual( judge.observe(input: reading(0), output: reading(0), at: 12, captureExpected: false), .silent) } func testTimeSpentPlayingDoesNotCountAsSilence() { var judge = MicJudge() judge.start(at: 0) judge.observe(input: reading(dbfs(-20)), output: reading(dbfs(-20)), at: 1) judge.resume(after: 10) // the test played for ten seconds XCTAssertEqual(judge.observe(input: reading(0), output: reading(0), at: 12), .working) } } final class MeterStepsTests: XCTestCase { func testStepsAreFiveDecibelsFromMinusSixty() { XCTAssertEqual(MeterSteps.step(0), 0) XCTAssertEqual(MeterSteps.step(dbfs(-70)), 0) XCTAssertEqual(MeterSteps.step(dbfs(-60)), 1) XCTAssertEqual(MeterSteps.step(dbfs(-26)), 7) XCTAssertEqual(MeterSteps.step(1), MeterSteps.count) XCTAssertEqual(MeterSteps.words(0), "silent") XCTAssertEqual(MeterSteps.words(7), "speaking level") } } final class SelfTestRecorderTests: XCTestCase { private func feed(_ recorder: SelfTestRecorder, value: Float, buffers: Int, frames: Int = 480, rate: Int = 48_000) { let buffer = [Float](repeating: value, count: frames) for _ in 0..) -> Float { 1 } } private final class TestClock: @unchecked Sendable { var now = 1000.0 } @MainActor private final class FakeCapture: AudioSelfTestCapture { let denoiser = CaptureDenoiser(suppressor: PassThrough()) var microphoneName = "Scarlett 18i8 USB" var microphoneDetail = "" var speakerName = "MacBook Pro Speakers" var speakerID: String? = "speakers" var inCall = false var captureExpected = true var muted = false var reduceNoise = true var stopsForPlayback = false var allowed = true var starts = 0 var stops = 0 /// Set: `start` waits until it is released, as a start held up by a /// Bluetooth headset's switch does. var holdStart = false var failStart: Error? var switching: String? /// Whether the microphone would be open now: started and not stopped /// since. var open = false private var held: CheckedContinuation? init() { denoiser.audioProcessingInitialize(sampleRate: 48_000, channels: 1) } func permitted() async -> Bool { allowed } func switchingMicrophone() async -> String? { switching } func start() async throws { starts += 1 if holdStart { await withCheckedContinuation { held = $0 } } if let failStart { throw failStart } open = true } func stop() { stops += 1 open = false } /// Lets a held start finish. func release() { held?.resume() held = nil } var isHeld: Bool { held != nil } func levels() -> (input: CaptureLevel.Reading, output: CaptureLevel.Reading) { (denoiser.inputLevel.take(), denoiser.level.take()) } func diagnostics() -> [(String, [String])] { [("Fake", ["fake device lines"])] } /// One 10 ms buffer through the hook, as WebRTC hands it over. func feed(_ amplitude: Float, rate: Int = 48_000) { let frames = rate / 100 var samples = (0.. Void = { _ in }, seconds: Double = 0.2 ) -> (AudioSelfTest, FakeCapture, FakePlayer, TestClock) { let capture = FakeCapture() configure(capture) let player = FakePlayer() let clock = TestClock() let test = AudioSelfTest( capture: capture, player: player, routes: nil, recordSeconds: seconds, clock: { clock.now }) return (test, capture, player, clock) } func testSpeechIsWorkingAndMoves() async { let (test, capture, _, clock) = make() await test.open() defer { test.close() } capture.feed(8_000) clock.now += 0.1 test.tick() XCTAssertEqual(test.micVerdict, .working) XCTAssertEqual(test.micSentence, "Your microphone is working.") XCTAssertGreaterThan(test.micStep, 0) } func testSilenceForFiveSecondsNamesTheDevice() async { let (test, capture, _, clock) = make() await test.open() defer { test.close() } for _ in 0..<50 { capture.feed(0) clock.now += 0.1 test.tick() } XCTAssertEqual(test.micVerdict, .silent) XCTAssertEqual(test.micStep, 0) XCTAssertEqual(test.micSentence, "Sound is arriving from Scarlett 18i8 USB, but it is silent.") } /// The verdict names the device the microphone really is, with its /// connection and rate, and says when nothing arrives at all. func testNoAudioNamesTheActualDeviceAndRate() async { let (test, _, _, clock) = make { $0.microphoneDetail = "Paul’s AirPods Pro (Bluetooth, 24 kHz)" } await test.open() defer { test.close() } for _ in 0..<60 { clock.now += 0.1 test.tick() } XCTAssertEqual(test.micVerdict, .noAudio) XCTAssertEqual( test.micSentence, "No sound is arriving from Paul’s AirPods Pro (Bluetooth, 24 kHz). Calls asked for it and nothing came back.") } /// A Bluetooth microphone at 24 kHz, which the suppressor passes through, /// still moves the meter and is judged working. func testATwentyFourKilohertzMicrophoneIsMeteredAndWorking() async { let (test, capture, _, clock) = make { $0.denoiser.audioProcessingInitialize(sampleRate: 24_000, channels: 1) } await test.open() defer { test.close() } capture.feed(8_000, rate: 24_000) clock.now += 0.1 test.tick() XCTAssertEqual(test.micVerdict, .working) XCTAssertGreaterThan(test.micStep, 0) } /// "Copy audio diagnostics": names, verdict and levels, and no audio. func testDiagnosticsSayWhatWasMeasuredAndHoldNoAudio() async { let (test, capture, _, clock) = make { $0.microphoneDetail = "Paul’s AirPods Pro (Bluetooth, 24 kHz)" } await test.open() defer { test.close() } capture.feed(8_000) clock.now += 0.1 test.tick() let text = test.diagnosticsText() XCTAssertTrue(text.contains("Paul’s AirPods Pro (Bluetooth, 24 kHz)"), text) XCTAssertTrue(text.contains("verdict working"), text) XCTAssertTrue(text.contains("## Level history"), text) XCTAssertTrue(text.contains("## Audio log (last 50 lines)"), text) XCTAssertTrue(text.contains("fake device lines"), text) XCTAssertTrue(text.contains("dBFS"), text) XCTAssertLessThan(text.count, 40_000, "names and numbers, not samples") } func testNoPermissionSaysSoAndStartsNothing() async { let (test, capture, _, _) = make { $0.allowed = false } await test.open() XCTAssertEqual(test.micProblem, AudioSelfTest.noPermission) XCTAssertEqual(capture.starts, 0) test.close() XCTAssertEqual(capture.stops, 0) } func testTheSpeakerTestPlaysTheChimeThroughTheChosenSpeaker() async { let (test, _, player, _) = make() await test.open() defer { test.close() } await test.playTestSound() XCTAssertEqual(test.speaker, .played) XCTAssertEqual(player.played.count, 1) XCTAssertEqual(player.played.first?.count, TestChime.samples().count) XCTAssertTrue(test.speakerSentence?.contains("MacBook Pro Speakers") ?? false) } func testRecordingPlaysBackWhatWasSentAndKeepsNothing() async { let (test, capture, player, _) = make() await test.open() defer { test.close() } let recording = Task { await test.recordAndPlayBack() } while test.recording != .recording { await Task.yield() } for _ in 0..<30 { capture.feed(8_000) } await recording.value XCTAssertEqual(test.recording, .played(silent: false)) XCTAssertEqual(player.played.count, 1) XCTAssertEqual(player.played.first?.count, 9_600) XCTAssertEqual(player.rates.first, 48_000) XCTAssertEqual(test.lastRecording?.seconds ?? 0, 0.2, accuracy: 0.001) XCTAssertFalse(test.holdsRecording) XCTAssertNil(capture.denoiser.sink, "the tap comes off with the recording") } func testASilentRecordingIsSaidToBe() async { let (test, capture, _, _) = make() await test.open() defer { test.close() } let recording = Task { await test.recordAndPlayBack() } while test.recording != .recording { await Task.yield() } for _ in 0..<30 { capture.feed(0) } await recording.value XCTAssertEqual(test.recording, .played(silent: true)) XCTAssertTrue(test.recordingSentence?.contains("Scarlett 18i8 USB") ?? false) } func testNothingRecordedFailsInsteadOfWaitingForever() async { let (test, _, player, clock) = make() await test.open() defer { test.close() } let recording = Task { await test.recordAndPlayBack() } while test.recording != .recording { await Task.yield() } clock.now += 10 await recording.value guard case .failed = test.recording else { return XCTFail("\(test.recording)") } XCTAssertTrue(player.played.isEmpty) } func testClosingMidRecordingThrowsItAway() async { let (test, capture, player, _) = make() await test.open() let recording = Task { await test.recordAndPlayBack() } while test.recording != .recording { await Task.yield() } for _ in 0..<5 { capture.feed(8_000) } test.close() await recording.value XCTAssertFalse(test.holdsRecording) XCTAssertTrue(player.played.isEmpty) XCTAssertNil(capture.denoiser.sink) XCTAssertEqual(capture.stops, 1) } func testInACallNothingPlaysAndMutedIsSaid() async { let (test, capture, player, clock) = make { $0.inCall = true $0.muted = true } await test.open() defer { test.close() } XCTAssertEqual(test.speaker, .unavailableInCall) XCTAssertEqual(test.recording, .unavailableInCall) await test.playTestSound() await test.recordAndPlayBack() XCTAssertTrue(player.played.isEmpty) clock.now += 0.1 test.tick() XCTAssertEqual(test.micSentence, AudioSelfTest.mutedInCall) capture.muted = false capture.feed(8_000) test.tick() XCTAssertEqual(test.micVerdict, .working) } func testMeterOnlyPublishesChanges() async { let (test, capture, _, clock) = make() await test.open() defer { test.close() } var published = 0 let watch = test.objectWillChange.sink { published += 1 } defer { watch.cancel() } capture.feed(8_000) test.tick() let first = published for _ in 0..<10 { capture.feed(8_000) clock.now += 0.1 test.tick() } XCTAssertEqual(published, first, "the same level and verdict are not published again") } // MARK: The start, off the main thread /// Waits (yielding the main actor) until `condition` holds. private func until(_ condition: () -> Bool) async { for _ in 0..<200 where !condition() { await Task.yield() } } /// The window opens at once and says it is starting; the verdict and /// meter take over only once the microphone has started. func testStartingIsSaidUntilTheMicrophoneHasStarted() async { let (test, capture, _, clock) = make { $0.holdStart = true } let opening = Task { await test.open() } await until { capture.isHeld } XCTAssertEqual(test.startPhase, .starting(switching: nil)) XCTAssertEqual(test.startingSentence, "Starting microphone…") XCTAssertEqual(test.micSentence, "Starting microphone…") XCTAssertNil(test.micAdvice) capture.release() await opening.value XCTAssertEqual(test.startPhase, .running) XCTAssertNil(test.startingSentence) capture.feed(8_000) clock.now += 0.1 test.tick() XCTAssertEqual(test.micVerdict, .working) test.close() } /// A Bluetooth headset is named while it switches. func testABluetoothSwitchIsSaid() async { let (test, capture, _, _) = make { $0.holdStart = true $0.switching = "Paul’s AirPods Pro" } let opening = Task { await test.open() } await until { capture.isHeld } XCTAssertEqual(test.startPhase, .starting(switching: "Paul’s AirPods Pro")) XCTAssertEqual(test.startingSentence, "Switching Paul’s AirPods Pro to microphone mode…") capture.release() await opening.value XCTAssertEqual(test.startPhase, .running) test.close() } /// Closed while the microphone starts: the start, when it finishes, is /// stopped at once, and nothing is left open or judged. func testClosingMidStartLeavesNothingOpen() async { let (test, capture, _, _) = make { $0.holdStart = true } let opening = Task { await test.open() } await until { capture.isHeld } test.close() XCTAssertEqual(test.startPhase, .cancelled) capture.release() await opening.value XCTAssertFalse(capture.open, "the microphone the late start opened was closed") XCTAssertEqual(capture.starts, 1) XCTAssertGreaterThanOrEqual(capture.stops, 1) XCTAssertEqual(test.startPhase, .cancelled) XCTAssertNil(test.micProblem, "a cancelled start is not reported as a failure") } /// Closed while permission is being asked: nothing starts at all. func testClosingWhileAskingStartsNothing() async { let (test, capture, _, _) = make() test.close() await test.open() XCTAssertEqual(capture.starts, 0) XCTAssertEqual(test.startPhase, .cancelled) } func testAStartThatFailsSaysWhy() async { let (test, capture, _, _) = make { $0.failStart = NSError(domain: "t", code: 1, userInfo: [NSLocalizedDescriptionKey: "no device"]) } await test.open() XCTAssertEqual(test.startPhase, .failed) XCTAssertEqual(test.micProblem, "The microphone would not start: no device") XCTAssertFalse(capture.open) test.close() } func testOpeningTwiceStartsOnce() async { let (test, capture, _, _) = make() await test.open() await test.open() XCTAssertEqual(capture.starts, 1) test.close() test.close() XCTAssertEqual(capture.stops, 1) } } /// The start's rules alone. final class MicStartTests: XCTestCase { func testTheHappyPath() { var start = MicStart() XCTAssertTrue(start.open()) XCTAssertEqual(start.phase, .asking) XCTAssertEqual(start.sentence, "Asking to use the microphone…") XCTAssertTrue(start.permission(true)) XCTAssertEqual(start.sentence, "Starting microphone…") start.microphone(bluetoothSwitching: "AirPods") XCTAssertEqual(start.sentence, "Switching AirPods to microphone mode…") XCTAssertEqual(start.finished(ok: true), .keep) XCTAssertEqual(start.phase, .running) XCTAssertNil(start.sentence) XCTAssertTrue(start.close(), "a running microphone is stopped on close") XCTAssertEqual(start.phase, .cancelled) } func testOpenOnlyOnce() { var start = MicStart() XCTAssertTrue(start.open()) XCTAssertFalse(start.open()) start.close() XCTAssertFalse(start.open(), "a closed test does not open again") } func testNoPermission() { var start = MicStart() _ = start.open() XCTAssertFalse(start.permission(false)) XCTAssertEqual(start.phase, .failed) XCTAssertFalse(start.close(), "nothing to stop") } func testPermissionAfterClose() { var start = MicStart() _ = start.open() XCTAssertFalse(start.close(), "nothing started yet") XCTAssertFalse(start.permission(true), "the answer comes back to a closed window") XCTAssertEqual(start.phase, .cancelled) } func testALateStartIsStopped() { var start = MicStart() _ = start.open() _ = start.permission(true) XCTAssertTrue(start.close(), "a start in flight may open the microphone") XCTAssertEqual(start.finished(ok: true), .stop) XCTAssertEqual(start.phase, .cancelled) } func testALateFailureIsQuiet() { var start = MicStart() _ = start.open() _ = start.permission(true) start.close() XCTAssertEqual(start.finished(ok: false), .failed) XCTAssertEqual(start.phase, .cancelled) } func testRestartAfterPlayback() { var start = MicStart() _ = start.open() _ = start.permission(true) _ = start.finished(ok: true) XCTAssertTrue(start.restart()) XCTAssertTrue(start.isStarting) XCTAssertEqual(start.finished(ok: true), .keep) start.close() XCTAssertFalse(start.restart(), "not after close") } func testTheBluetoothNameOnlyWhileStarting() { var start = MicStart() start.microphone(bluetoothSwitching: "AirPods") XCTAssertEqual(start.phase, .idle) } }