oficina / meet Public

Clone
git clone https://amadan.net/oficina/meet

Plain git — no account needed to clone.

Download

Download this file

1import AppKit
2import CallsKit
3import Foundation
4import SwiftUI
5
6/// `CallsInterop --audio-selftest`: the audio test's record-and-play-back
7/// path, run end to end without a microphone.
8///
9/// The test's own standalone capture is built as on the home screen — a
10/// CallAudio on LiveKit's engine module, WebRTC's processing module, the
11/// CaptureDenoiser in the capture hook — with the module in manual
12/// rendering and the synthetic voice as its microphone (as the call gate
13/// runs it). The microphone is started as the test starts it, by the
14/// in-memory loopback (SelfTestLoopback), and the recording is taken from
15/// the hook's sink. It must be
16/// non-silent, the right length and rate, handed to the player once, and
17/// gone afterwards. Run once with noise reduction on (whichever suppressor
18/// the build has; DeepFilterNet with CALLS_DEEPFILTER_MODEL set) and once
19/// with it off. Then a control, so a pass means something: a silent
20/// microphone must be called silent, and its recording silent. Then the
21/// start on a real device (deviceStart), with its own control. Prints one
22/// PASS or FAIL line per run; exits 0 only if all five passed.
23@MainActor enum AudioSelfTestHarness {
24 /// -40 dBFS RMS. The synthetic voice is about -20 dBFS; silence is
25 /// below -90.
26 static let minimumRMS: Float = 0.01
27
28 static func run() async -> Bool {
29 var passed = true
30 for reduceNoise in [true, false] {
31 passed = await runOnce(reduceNoise: reduceNoise) && passed
32 }
33 passed = await silentControl() && passed
34 passed = await deviceStart() && passed
35 return passed
36 }
37
38 /// The test on the real audio device module, not manual rendering: it
39 /// must ask the module to record. Manual rendering runs its input
40 /// whenever its output runs, so the runs above pass whether or not the
41 /// test ever opens a microphone; on a device it has to be asked, and up
42 /// to 0.3.7 it never was (the loopback is never connected, and WebRTC
43 /// opens the microphone only for a send stream on a connection that has
44 /// been). This process has no microphone permission, so the module
45 /// refuses right after being asked; being asked is what is checked.
46 /// First the control, 0.3.7's start, which must come back not asked,
47 /// so a pass means the ask is what made the difference.
48 private static func deviceStart() async -> Bool {
49 var passed = true
50 let control = await AudioSelfTest.deviceStartProbe(askToRecord: false)
51 let controlLabel = "audio self-test control (0.3.7's start on a real device never opens the microphone)"
52 if control.asked {
53 print("FAIL \(controlLabel): the module was asked to record anyway; \(control.detail)")
54 passed = false
55 } else {
56 print("PASS \(controlLabel): \(control.detail)")
57 }
58 let fixed = await AudioSelfTest.deviceStartProbe(askToRecord: true)
59 let label = "audio self-test on a real device (asks the module to record)"
60 if fixed.asked {
61 print("PASS \(label): \(fixed.detail)")
62 } else {
63 print("FAIL \(label): the microphone was never asked for; \(fixed.detail)")
64 passed = false
65 }
66 return passed
67 }
68
69 private static func silentControl() async -> Bool {
70 let label = "audio self-test control (a silent microphone)"
71 let player = CheckingPlayer()
72 let test = AudioSelfTest.harness(input: Silence(), player: player, reduceNoise: true, recordSeconds: 2)
73 await test.open()
74 defer { test.close() }
75 let deadline = Date().addingTimeInterval(8)
76 while test.micVerdict != .silent, Date() < deadline {
77 try? await Task.sleep(for: .milliseconds(100))
78 }
79 var problems: [String] = []
80 if test.micVerdict != .silent { problems.append("microphone verdict \(test.micVerdict), wanted silent") }
81 await test.recordAndPlayBack()
82 if test.recording != .played(silent: true) { problems.append("recording state \(test.recording), wanted played(silent: true)") }
83 if let summary = test.lastRecording, summary.rms >= minimumRMS {
84 problems.append("a silent microphone recorded RMS \(db(summary.rms)) dBFS")
85 }
86 if test.lastRecording == nil { problems.append("the capture ran no buffers at all") }
87 if problems.isEmpty {
88 print("PASS \(label): called silent, and its recording was silent")
89 return true
90 }
91 print("FAIL \(label): \(problems.joined(separator: "; "))")
92 return false
93 }
94
95 private static func runOnce(reduceNoise: Bool) async -> Bool {
96 let label = "audio self-test (noise reduction \(reduceNoise ? "on" : "off"))"
97 let player = CheckingPlayer()
98 let test = AudioSelfTest.harness(
99 input: SyntheticVoice(), player: player, reduceNoise: reduceNoise, recordSeconds: 5)
100 await test.open()
101 defer { test.close() }
102 // The microphone verdict first, as the person would see it.
103 let deadline = Date().addingTimeInterval(8)
104 while test.micVerdict != .working, Date() < deadline {
105 try? await Task.sleep(for: .milliseconds(100))
106 }
107 let verdict = test.micVerdict
108 await test.recordAndPlayBack()
109 // For looking at, not for passing: the test's own view, after the run.
110 if let dir = ProcessInfo.processInfo.environment["CALLS_SELFTEST_SNAPSHOT"] {
111 snapshot(test, to: URL(fileURLWithPath: dir).appendingPathComponent(
112 "audio-selftest-\(reduceNoise ? "on" : "off").png"))
113 }
114 var problems: [String] = []
115 if let problem = test.micProblem { problems.append("microphone: \(problem)") }
116 if verdict != .working { problems.append("microphone verdict \(verdict), wanted working") }
117 guard let summary = test.lastRecording else {
118 print("FAIL \(label): nothing recorded (\(test.recording)) \(problems.joined(separator: "; "))")
119 return false
120 }
121 if test.recording != .played(silent: false) { problems.append("recording state \(test.recording)") }
122 if summary.rms < minimumRMS { problems.append("recording is silent (RMS \(db(summary.rms)) dBFS)") }
123 if abs(summary.seconds - test.recordSeconds) > 0.05 {
124 problems.append("recorded \(summary.seconds) s, wanted \(test.recordSeconds)")
125 }
126 if summary.rate != 48_000 { problems.append("recorded at \(summary.rate) Hz, wanted 48000") }
127 if player.plays != 1 { problems.append("played \(player.plays) times, wanted once") }
128 if abs(player.rms - summary.rms) > 1e-4 { problems.append("the player was not given the recording") }
129 if test.holdsRecording { problems.append("the recording was kept after playback") }
130 let suppressed = test.framesSuppressed
131 if reduceNoise, suppressed == 0 { problems.append("noise reduction was on but processed no frames") }
132 if !reduceNoise, suppressed != 0 { problems.append("noise reduction was off but processed \(suppressed) frames") }
133 let detail =
134 "\(String(format: "%.2f", summary.seconds)) s at \(summary.rate) Hz, RMS \(db(summary.rms)) dBFS, "
135 + "\(test.suppressorName) processed \(suppressed) frames"
136 if problems.isEmpty {
137 print("PASS \(label): \(detail)")
138 return true
139 }
140 print("FAIL \(label): \(problems.joined(separator: "; ")) [\(detail)]")
141 return false
142 }
143
144 private static func snapshot(_ test: AudioSelfTest, to url: URL) {
145 NSApplication.shared.setActivationPolicy(.prohibited)
146 let view = NSHostingView(
147 rootView: AudioSelfTestView(test: test).padding(20).frame(width: 460)
148 .background(Color.white).tint(CallsAppearance.accent).environment(\.colorScheme, .light))
149 view.frame = NSRect(x: 0, y: 0, width: 460, height: view.fittingSize.height)
150 view.layoutSubtreeIfNeeded()
151 guard let bitmap = view.bitmapImageRepForCachingDisplay(in: view.bounds) else { return }
152 view.cacheDisplay(in: view.bounds, to: bitmap)
153 try? FileManager.default.createDirectory(at: url.deletingLastPathComponent(), withIntermediateDirectories: true)
154 try? bitmap.representation(using: .png, properties: [:])?.write(to: url)
155 }
156
157 static func db(_ level: Float) -> String { String(format: "%.1f", SampleLevel.db(level)) }
158}
159
160/// A microphone that delivers buffers of zeros: the empty second input
161/// of an interface captured in mono.
162private final class Silence: AudioInputSource, @unchecked Sendable {
163 func render(_ samples: UnsafeMutablePointer<Float>, count: Int, sampleRate: Double) {
164 samples.update(repeating: 0, count: count)
165 }
166}
167
168/// Plays nothing: records what it was given, and feeds the meter as a
169/// player would.
170@MainActor private final class CheckingPlayer: AudioSelfTestPlayer {
171 var plays = 0
172 var rms: Float = 0
173
174 func play(_ samples: [Float], sampleRate: Double, speaker: String?, meter: OutputMeter) async throws {
175 plays += 1
176 rms = SampleLevel.rms(samples)
177 samples.withUnsafeBufferPointer { meter.record($0.baseAddress!, count: $0.count) }
178 try? await Task.sleep(for: .milliseconds(200))
179 }
180 func stop() {}
181}
182