import XCTest @testable import CallsKit /// AudioHealth is the one part of the device work with real logic in it, /// and the one that has to be right: it is what a person reads when a /// call sounds wrong, and on macOS it is the only instrument there is. final class AudioHealthTests: XCTestCase { func testNothingReceivedIsSaidPlainlyAndNeverDividedBy() { let health = AudioHealth(concealed: 0, total: 0, inserted: 0, removed: 0, jitterBufferMs: 0) XCTAssertTrue(health.silent) // Not 0% clean: no audio is not clean audio, and a rate computed from // an empty denominator would say the opposite of the truth. XCTAssertEqual(health.concealmentRate, 0) XCTAssertEqual(health.driftRate, 0) XCTAssertEqual(health.summary, "No audio received yet.") } func testDriftIsNamedAheadOfLossWhenBothAreUp() { // The growl case. Loss is common, transient and widely understood; // sustained drift is rarer, is what makes a voice sound *wrong* // rather than broken, and is the one nobody thinks to look for. With // both raised, the rarer diagnosis is the useful one to lead with. let health = AudioHealth( concealed: 20_000, total: 100_000, inserted: 4_000, removed: 1_000, jitterBufferMs: 300) XCTAssertEqual(health.driftRate, 0.05, accuracy: 0.0001) XCTAssertEqual(health.concealmentRate, 0.20, accuracy: 0.0001) XCTAssertTrue(health.summary.contains("resampled"), health.summary) XCTAssertTrue(health.summary.contains("growling"), health.summary) } func testLossAloneReadsAsDropoutsAndNotAsDrift() { let health = AudioHealth( concealed: 10_000, total: 100_000, inserted: 0, removed: 0, jitterBufferMs: 40) XCTAssertTrue(health.summary.contains("did not arrive"), health.summary) XCTAssertTrue(health.summary.contains("dropouts"), health.summary) XCTAssertFalse(health.summary.contains("growling"), health.summary) } func testAHealthyCallSaysSoWithItsNumbers() { let health = AudioHealth( concealed: 500, total: 100_000, inserted: 100, removed: 100, jitterBufferMs: 30) XCTAssertFalse(health.silent) XCTAssertTrue(health.summary.contains("cleanly"), health.summary) // The numbers ride along even when the verdict is good: someone // comparing two calls needs them, and rounding to zero is honest at // this resolution. XCTAssertTrue(health.summary.contains("0%"), health.summary) } func testDriftCountsBothDirections() { // Inserting and removing are the same fault seen from two sides — the // signal was rescaled either way. A reading that counted only one // would miss a sender whose clock runs fast. let fast = AudioHealth(concealed: 0, total: 1_000, inserted: 0, removed: 50, jitterBufferMs: 0) let slow = AudioHealth(concealed: 0, total: 1_000, inserted: 50, removed: 0, jitterBufferMs: 0) XCTAssertEqual(fast.driftRate, slow.driftRate) XCTAssertTrue(fast.summary.contains("resampled"), fast.summary) XCTAssertTrue(slow.summary.contains("resampled"), slow.summary) } func testItReadsTheStatsDictionaryTheEngineActuallyProduces() { // The field names are libwebrtc's, spelled once in MediaEngine and // once here. This is the test that fails if they stop agreeing. let health = AudioHealth([ "concealedSamples": 7, "totalSamplesReceived": 70, "insertedSamplesForDeceleration": 3, "removedSamplesForAcceleration": 4, "jitterBufferDelayMs": 120, ]) XCTAssertEqual(health.concealed, 7) XCTAssertEqual(health.total, 70) XCTAssertEqual(health.inserted, 3) XCTAssertEqual(health.removed, 4) XCTAssertEqual(health.jitterBufferMs, 120) for field in MediaEngine.audioHealthFields { XCTAssertTrue( ["concealedSamples", "totalSamplesReceived", "insertedSamplesForDeceleration", "removedSamplesForAcceleration"].contains(field), "MediaEngine gathers \(field), which AudioHealth does not read") } } func testAMissingFieldIsZeroAndNotACrash() { // An older or newer libwebrtc that stops reporting one of these // should cost the readout a number, not the call a panel. let health = AudioHealth(["totalSamplesReceived": 1_000]) XCTAssertEqual(health.concealed, 0) XCTAssertFalse(health.silent) XCTAssertTrue(health.summary.contains("cleanly"), health.summary) } } /// AudioRoutes on macOS reads and chooses devices on the call's own /// WebRTC audio device module. These run against the real machine and a /// real module (no recording is started), because the thing worth testing /// is that the module's lists come through named and choosable. final class AudioRoutesTests: XCTestCase { @MainActor func testMacListsTheModuleDevicesAndCanChoose() throws { #if os(macOS) let suite = "calls-audio-module-\(UUID().uuidString)" let store = UserDefaults(suiteName: suite)! defer { store.removePersistentDomain(forName: suite) } let routes = AudioRoutes(defaults: store) XCTAssertTrue(routes.inputs.isEmpty, "no module yet, so nothing to list") XCTAssertThrowsError(try routes.use(input: "anything")) let audio = CallAudio(routes: routes, audioDevice: nil) XCTAssertTrue(routes.selectable) // The regression behind Paul's "not picking up the Scarlett at // launch": the engine module lists only its "default" entry until a // connection initialises it. The pickers come from CoreAudio, which // is complete now. XCTAssertEqual( audio.factory.audioDeviceModule.inputDevices.map(\.deviceId), ["default"], "LiveKit's module has not enumerated before the first connection") XCTAssertEqual( routes.inputs.count, CoreAudioDevices.inventory().inputs.count + 1, "every CoreAudio input, and System default, before any connection") // What this machine lists, for the record: every input device macOS // has, named as macOS names it, the default among them. for device in routes.inputs { print("audio input: \(device.name) [\(device.id)]") } for device in routes.outputs { print("audio output: \(device.name) [\(device.id)]") } // A machine with no input at all is possible, so this asserts the // shape rather than the presence. for device in routes.inputs + routes.outputs { XCTAssertFalse(device.name.isEmpty, "a listed device must have a name") XCTAssertTrue(device.selectable) } if let current = routes.currentInput, !routes.inputs.isEmpty { XCTAssertTrue(routes.inputs.contains { $0.id == current }, "the current input is one listed") } XCTAssertThrowsError(try routes.use(input: "no-such-device")) { error in XCTAssertEqual((error as NSError).code, 5) } if let first = routes.inputs.first { XCTAssertNoThrow(try routes.use(input: first.id)) XCTAssertEqual(routes.currentInput, first.id) } // A device other than the default: the choice must hold. Choosing // only records it; no engine is running, so nothing records. if let other = routes.inputs.first(where: { !$0.name.hasPrefix("System default") }) { XCTAssertNoThrow(try routes.use(input: other.id)) XCTAssertEqual(routes.currentInput, other.id) routes.reset() XCTAssertEqual(routes.currentInput, other.id, "leaving keeps the choice for next time") } #else throw XCTSkip("macOS only") #endif } } /// The readout before the microphone has started. Paul's 0.3.1 screenshot, /// alone in a call, read "The microphone runs at 16 kHz … Echo /// cancellation: off. Noise suppression: off." Those are exactly what the /// module says before any microphone opens: WebRTC opens it when the first /// connection starts sending, and until then its processing sits at its /// construction default of 16 kHz with nothing applied. final class ProcessingReadoutTests: XCTestCase { func testNotStartedSaysSoInsteadOfStaleValues() { let report = ProcessingReport( suppression: .unsupportedRate(16_000), echoCancellation: "off", noiseSuppression: "off", gainControl: "off", refused: nil, started: false) XCTAssertFalse(report.summary.contains("16 kHz")) XCTAssertFalse(report.summary.contains("Echo cancellation: off")) XCTAssertTrue(report.summary.hasPrefix("The microphone has not started yet.")) } @MainActor func testAloneInACallTheMicrophoneHasNotStarted() throws { #if os(macOS) let suite = "calls-readout-\(UUID().uuidString)" let store = UserDefaults(suiteName: suite)! defer { store.removePersistentDomain(forName: suite) } let audio = CallAudio(routes: AudioRoutes(defaults: store), audioDevice: nil) let track = audio.makeTrack() _ = track let report = audio.report() // The evidence: with a track made and nothing sending, the module is // not recording, and the processing reports the 16 kHz default with // echo cancellation off — the screenshot's numbers. XCTAssertFalse(audio.capturing) print("pre-start readout: suppression=\(audio.denoiser.state) ec=\(report.echoCancellation) ns=\(report.noiseSuppression) gain=\(report.gainControl)") XCTAssertFalse(report.started) XCTAssertTrue(report.summary.hasPrefix("The microphone has not started yet.")) #else throw XCTSkip("macOS only") #endif } }