oficina / meet Public

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

Plain git — no account needed to clone.

Download

Download this file

1import XCTest
2
3@_spi(Fixtures) @testable import CallsKit
4
5/// The speaking glow's rule (0.3.7) on a clock the tests own: what lights
6/// it and what does not, how quickly it rises and falls, silence, and
7/// Reduce Motion.
8final class SpeakingGlowTests: XCTestCase {
9 private func level(db: Float) -> Float { pow(10, db / 20) }
10
11 /// Feeds a gate and a glow the same readings, as AudioLevels does.
12 private struct Voice {
13 var gate = SpeakingGate()
14 var glow = SpeakingGlow()
15 mutating func hear(db: Float, at now: Double) {
16 let level = db.isFinite ? pow(10, db / 20) : 0
17 let speaking = gate.update(level: level, now: now)
18 glow.update(level: level, speaking: speaking, now: now)
19 }
20 }
21
22 func testBrightnessFollowsDecibels() {
23 XCTAssertEqual(SpeakingGlow.intensity(level: 0), 0)
24 XCTAssertEqual(SpeakingGlow.intensity(level: level(db: -60)), 0)
25 XCTAssertEqual(SpeakingGlow.intensity(level: level(db: SpeakingGlow.quietDb)), 0, accuracy: 1e-6)
26 XCTAssertEqual(SpeakingGlow.intensity(level: level(db: -27)), 0.5, accuracy: 1e-3)
27 XCTAssertEqual(SpeakingGlow.intensity(level: level(db: SpeakingGlow.loudDb)), 1, accuracy: 1e-6)
28 XCTAssertEqual(SpeakingGlow.intensity(level: 1), 1)
29 var last = 0.0
30 for db in stride(from: Float(-90), through: 0, by: 1) {
31 let x = SpeakingGlow.intensity(level: level(db: db))
32 XCTAssertGreaterThanOrEqual(x, last, "louder is never dimmer")
33 last = x
34 }
35 }
36
37 func testSilenceIsNoGlow() {
38 var voice = Voice()
39 for t in stride(from: 0.0, through: 5, by: 0.1) {
40 voice.hear(db: -.infinity, at: t)
41 XCTAssertFalse(voice.glow.lit)
42 XCTAssertEqual(voice.glow.value(at: t), 0)
43 XCTAssertEqual(voice.glow.value(at: t, reduceMotion: true), 0)
44 }
45 // A quiet room's tone, under the speaking level, is not speech either.
46 for t in stride(from: 5.0, through: 10, by: 0.1) { voice.hear(db: -55, at: t) }
47 XCTAssertFalse(voice.glow.lit)
48 }
49
50 func testABriefNoiseDoesNotLightIt() {
51 // One loud reading — a click, a cough, a door — then quiet. The gate
52 // calls that speaking for its hangover; the glow waits for more.
53 var voice = Voice()
54 voice.hear(db: -8, at: 0)
55 XCTAssertTrue(voice.gate.speaking, "the gate is on at once")
56 for t in stride(from: 0.1, through: 2, by: 0.1) {
57 voice.hear(db: -70, at: t)
58 XCTAssertFalse(voice.glow.lit, "at \(t)s")
59 XCTAssertEqual(voice.glow.value(at: t), 0)
60 }
61 // Two clicks a second and a half apart are two brief noises, not a
62 // voice, read at the peers' five times a second.
63 voice.hear(db: -8, at: 3)
64 for t in stride(from: 3.2, through: 4.2, by: 0.2) { voice.hear(db: -70, at: t) }
65 voice.hear(db: -8, at: 4.4)
66 for t in stride(from: 4.6, through: 5.4, by: 0.2) {
67 voice.hear(db: -70, at: t)
68 XCTAssertFalse(voice.glow.lit, "at \(t)s")
69 }
70 }
71
72 func testSpeechLightsItOnceItKeepsUp() {
73 var voice = Voice()
74 voice.hear(db: -20, at: 0)
75 XCTAssertFalse(voice.glow.lit, "one reading is not yet a voice")
76 voice.hear(db: -20, at: 0.1)
77 XCTAssertFalse(voice.glow.lit)
78 voice.hear(db: -20, at: 0.2)
79 XCTAssertTrue(voice.glow.lit, "sound at a speaking level for \(SpeakingGlow.onset)s is")
80 XCTAssertEqual(voice.glow.target, SpeakingGlow.intensity(level: level(db: -20)), accuracy: 1e-9)
81 // Between syllables its level falls away but it stays lit, as the
82 // gate does: the rim and inner light hold at their least.
83 voice.hear(db: -45, at: 0.3)
84 XCTAssertTrue(voice.glow.lit)
85 XCTAssertEqual(voice.glow.target, 0, accuracy: 1e-9)
86 XCTAssertTrue(voice.glow.on(at: 5))
87 // And goes out when the gate lets go.
88 for t in stride(from: 0.4, through: 1.2, by: 0.1) { voice.hear(db: -70, at: t) }
89 XCTAssertFalse(voice.glow.lit)
90 XCTAssertEqual(voice.glow.target, 0)
91 }
92
93 func testItRisesQuicklyAndFallsSlowly() {
94 var glow = SpeakingGlow()
95 let loud = level(db: -12)
96 glow.update(level: loud, speaking: true, now: 0)
97 glow.update(level: loud, speaking: true, now: 0.15)
98 XCTAssertTrue(glow.lit)
99 XCTAssertEqual(glow.value(at: 0.15), 0, accuracy: 1e-9, "from where it was")
100 // Attack: 63 % of the way in one time constant, all of it in five.
101 XCTAssertEqual(
102 glow.value(at: 0.15 + SpeakingGlow.attack), 1 - exp(-1), accuracy: 1e-6)
103 XCTAssertGreaterThan(glow.value(at: 0.15 + 5 * SpeakingGlow.attack), 0.99)
104 XCTAssertLessThan(glow.settles, 0.15 + 6 * SpeakingGlow.attack, "and the view stops redrawing")
105
106 // Release: silence at 1 s, and it fades over hundreds of milliseconds,
107 // not at once and not at the attack's pace.
108 glow.update(level: 0, speaking: false, now: 1)
109 XCTAssertFalse(glow.lit)
110 XCTAssertEqual(glow.value(at: 1), 1, accuracy: 1e-3)
111 XCTAssertEqual(glow.value(at: 1 + SpeakingGlow.release), exp(-1), accuracy: 1e-3)
112 XCTAssertGreaterThan(glow.value(at: 1 + SpeakingGlow.attack), 0.8, "falls slower than it rises")
113 XCTAssertLessThan(glow.value(at: 1 + 5 * SpeakingGlow.release), 0.01)
114 XCTAssertGreaterThan(glow.settles, 1 + 4 * SpeakingGlow.release)
115 XCTAssertLessThan(glow.settles, 1 + 7 * SpeakingGlow.release)
116 XCTAssertTrue((0.25...0.4).contains(SpeakingGlow.release))
117 XCTAssertEqual(SpeakingGlow.attack, 0.05)
118 }
119
120 func testAReadingMidwayCarriesOnFromWhereItWas() {
121 var glow = SpeakingGlow()
122 glow.update(level: level(db: -12), speaking: true, now: 0)
123 glow.update(level: level(db: -12), speaking: true, now: 0.2)
124 let midway = glow.value(at: 0.225)
125 XCTAssertTrue(glow.update(level: level(db: -40), speaking: true, now: 0.225))
126 XCTAssertEqual(glow.value(at: 0.225), midway, accuracy: 1e-9, "no jump when the target moves")
127 }
128
129 func testAReadingThatChangesNothingVisibleIsNotAChange() {
130 var glow = SpeakingGlow()
131 XCTAssertFalse(glow.update(level: 0, speaking: false, now: 0), "silence stays silence")
132 glow.update(level: level(db: -20), speaking: true, now: 1)
133 XCTAssertTrue(glow.update(level: level(db: -20), speaking: true, now: 1.2), "lights")
134 XCTAssertFalse(
135 glow.update(level: level(db: -20.1), speaking: true, now: 1.3), "a tenth of a dB is not a redraw")
136 }
137
138 func testReduceMotionIsOneSteadyGlow() {
139 var glow = SpeakingGlow()
140 glow.update(level: level(db: -40), speaking: true, now: 0)
141 XCTAssertEqual(glow.value(at: 0, reduceMotion: true), 0, "not lit yet: nothing")
142 glow.update(level: level(db: -10), speaking: true, now: 0)
143 glow.update(level: level(db: -10), speaking: true, now: 0.2)
144 for (db, t) in [(-10, 0.3), (-35, 0.4), (-20, 0.5)] as [(Float, Double)] {
145 glow.update(level: level(db: db), speaking: true, now: t)
146 for dt in [0, 0.01, 0.1, 1.0] {
147 XCTAssertEqual(glow.value(at: t + dt, reduceMotion: true), SpeakingGlow.steady)
148 }
149 }
150 glow.update(level: 0, speaking: false, now: 1)
151 XCTAssertEqual(glow.value(at: 1, reduceMotion: true), 0, "out at once, no fade")
152 }
153
154 func testTheFixtureHoldsALevel() {
155 XCTAssertFalse(SpeakingGlow.fixture(level: level(db: -60)).lit)
156 let mid = SpeakingGlow.fixture(level: level(db: -20))
157 XCTAssertTrue(mid.lit)
158 XCTAssertEqual(mid.value(at: 0), mid.value(at: 100), "still")
159 }
160
161 @MainActor func testTheTilesAndTheTabGlow() {
162 let levels = AudioLevels()
163 let unmuted = PeerMedia(muted: false, cameraOff: false)
164 func tick(_ t: Double, bea: Float, cy: Float) {
165 levels.updatePeer(
166 "bea", name: "Bea",
167 sample: PeerAudioSample(reported: unmuted, connected: true, arriving: true, level: level(db: bea)),
168 notices: [], now: t)
169 levels.updatePeer(
170 "cy", name: "Cy",
171 sample: PeerAudioSample(reported: unmuted, connected: true, arriving: true, level: level(db: cy)),
172 notices: [], now: t)
173 levels.settle(present: ["bea", "cy"])
174 }
175 for t in stride(from: 0.0, through: 1, by: 0.2) { tick(t, bea: -15, cy: -60) }
176 XCTAssertTrue(levels.peer("bea").glow.glow.lit)
177 XCTAssertFalse(levels.peer("cy").glow.glow.lit)
178 XCTAssertEqual(levels.othersGlow.glow, levels.peer("bea").glow.glow, "the tab follows Bea")
179 levels.settle(present: ["cy"])
180 XCTAssertFalse(levels.peer("bea").glow.glow.lit, "someone who left does not glow")
181 XCTAssertFalse(levels.othersGlow.glow.lit)
182
183 // Your own tile, and muting puts it out.
184 let mic = { (muted: Bool, db: Float) in
185 MicSample(muted: muted, capturing: true, inputPeak: self.level(db: db), inputBuffers: 10, sentPeak: self.level(db: db), sending: true)
186 }
187 for t in stride(from: 0.0, through: 0.5, by: 0.1) {
188 levels.updateMic(mic(false, -15), now: t, source: "", channels: false)
189 }
190 XCTAssertTrue(levels.mic.glow.glow.lit)
191 levels.updateMic(mic(true, -15), now: 0.6, source: "", channels: false)
192 XCTAssertFalse(levels.mic.glow.glow.lit)
193 }
194}
195