@mentra/engine 3.2.1-dev.278 → 3.2.1-dev.282
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/generated/releaseMetadata.js +5 -5
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/services/AcsMeetingService.d.ts +56 -6
- package/build/services/AcsMeetingService.d.ts.map +1 -1
- package/build/services/AcsMeetingService.js +162 -40
- package/build/services/AcsMeetingService.js.map +1 -1
- package/build/services/CallGainSweep.d.ts +97 -0
- package/build/services/CallGainSweep.d.ts.map +1 -0
- package/build/services/CallGainSweep.js +249 -0
- package/build/services/CallGainSweep.js.map +1 -0
- package/build/services/GlassesMicProbe.d.ts +2 -0
- package/build/services/GlassesMicProbe.d.ts.map +1 -1
- package/build/services/GlassesMicProbe.js +20 -19
- package/build/services/GlassesMicProbe.js.map +1 -1
- package/build/services/LocalMiniappRuntime.d.ts +18 -0
- package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
- package/build/services/LocalMiniappRuntime.js +131 -1
- package/build/services/LocalMiniappRuntime.js.map +1 -1
- package/build/services/MicSessionManager.d.ts +83 -0
- package/build/services/MicSessionManager.d.ts.map +1 -0
- package/build/services/MicSessionManager.js +201 -0
- package/build/services/MicSessionManager.js.map +1 -0
- package/build/services/MicStateCoordinator.d.ts +71 -5
- package/build/services/MicStateCoordinator.d.ts.map +1 -1
- package/build/services/MicStateCoordinator.js +161 -10
- package/build/services/MicStateCoordinator.js.map +1 -1
- package/build/services/micPolicy.d.ts +131 -0
- package/build/services/micPolicy.d.ts.map +1 -0
- package/build/services/micPolicy.js +140 -0
- package/build/services/micPolicy.js.map +1 -0
- package/build/stores/bluetoothSettingKeys.d.ts.map +1 -1
- package/build/stores/bluetoothSettingKeys.js +3 -0
- package/build/stores/bluetoothSettingKeys.js.map +1 -1
- package/build/stores/settings.d.ts +5 -0
- package/build/stores/settings.d.ts.map +1 -1
- package/build/stores/settings.js +62 -0
- package/build/stores/settings.js.map +1 -1
- package/build/utils/pcm16.d.ts +20 -0
- package/build/utils/pcm16.d.ts.map +1 -1
- package/build/utils/pcm16.js +32 -1
- package/build/utils/pcm16.js.map +1 -1
- package/package.json +8 -8
- package/src/generated/releaseMetadata.ts +5 -5
- package/src/services/AcsMeetingService.ts +167 -40
- package/src/services/CallGainSweep.ts +318 -0
- package/src/services/GlassesMicProbe.ts +20 -17
- package/src/services/LocalMiniappRuntime.ts +152 -3
- package/src/services/MicSessionManager.ts +258 -0
- package/src/services/MicStateCoordinator.ts +153 -10
- package/src/services/micPolicy.ts +198 -0
- package/src/stores/bluetoothSettingKeys.ts +3 -0
- package/src/stores/settings.ts +66 -0
- package/src/utils/pcm16.ts +43 -1
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MicSessionManager
|
|
3
|
+
*
|
|
4
|
+
* The engine's microphone ownership layer. Applications acquire a lease that
|
|
5
|
+
* says what they are doing; this class tracks the live leases, resolves them
|
|
6
|
+
* through `micPolicy`, and pushes the result to MicStateCoordinator.
|
|
7
|
+
*
|
|
8
|
+
* Everything above it is semantic ("I need a glasses microphone for a voice
|
|
9
|
+
* call"). Everything below it is hardware. Audio *sinks* — AcsMeetingService
|
|
10
|
+
* today, another transport later — are downstream of the PCM this produces and
|
|
11
|
+
* must not reach past it to the coordinator or the Bluetooth SDK.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import {Platform} from "react-native"
|
|
15
|
+
import BluetoothSdk from "@mentra/bluetooth-sdk/internal"
|
|
16
|
+
|
|
17
|
+
import {getCallGainSweep} from "./CallGainSweep"
|
|
18
|
+
import micStateCoordinator from "./MicStateCoordinator"
|
|
19
|
+
import {
|
|
20
|
+
resolveMicPolicy,
|
|
21
|
+
type MicPlatformCaps,
|
|
22
|
+
type MicSessionSpec,
|
|
23
|
+
type MicSource,
|
|
24
|
+
type MicUseCase,
|
|
25
|
+
} from "./micPolicy"
|
|
26
|
+
|
|
27
|
+
const LOG_TAG = "MIC_SESSION"
|
|
28
|
+
|
|
29
|
+
/** Raised when a second source is requested while another is already live. */
|
|
30
|
+
export const MIC_SOURCE_CONFLICT = "MIC_SOURCE_CONFLICT"
|
|
31
|
+
|
|
32
|
+
export interface MicSessionOptions {
|
|
33
|
+
/** Package name for a miniapp, or `engine:<name>` for an engine feature. */
|
|
34
|
+
owner: string
|
|
35
|
+
source: MicSource
|
|
36
|
+
useCase: MicUseCase
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface MicSession {
|
|
40
|
+
readonly id: number
|
|
41
|
+
readonly owner: string
|
|
42
|
+
readonly source: MicSource
|
|
43
|
+
readonly useCase: MicUseCase
|
|
44
|
+
/** Idempotent. Releasing a session that is already gone is a no-op. */
|
|
45
|
+
release(): void
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface LiveSession extends MicSessionSpec {
|
|
49
|
+
id: number
|
|
50
|
+
owner: string
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function defaultCaps(): MicPlatformCaps {
|
|
54
|
+
return {
|
|
55
|
+
// iOS has no `setMicSourcePin`, so it cannot promise the phone microphone
|
|
56
|
+
// stays shut, and `glassesLc3UplinkSupported` never selects the BLE LC3
|
|
57
|
+
// uplink there.
|
|
58
|
+
glassesPcmUplink: Platform.OS === "android" && typeof BluetoothSdk.setMicSourcePin === "function",
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
class MicSessionManager {
|
|
63
|
+
private static instance: MicSessionManager | null = null
|
|
64
|
+
|
|
65
|
+
private readonly sessions = new Map<number, LiveSession>()
|
|
66
|
+
private nextId = 1
|
|
67
|
+
private caps: MicPlatformCaps = defaultCaps()
|
|
68
|
+
/** Last pin we asked for, so a no-op change does not re-enter native. */
|
|
69
|
+
private pinned = false
|
|
70
|
+
/**
|
|
71
|
+
* One 15→14→15→13 walk per voice-call generation. Cleared when the last
|
|
72
|
+
* glasses voice_call session drops so the next join can measure again.
|
|
73
|
+
*/
|
|
74
|
+
private sweepFinished = false
|
|
75
|
+
/** One-shot A/B walk. Off now that 15/14/13 is measured; Super Mode can still start it. */
|
|
76
|
+
private sweepEnabled = false
|
|
77
|
+
|
|
78
|
+
private constructor() {}
|
|
79
|
+
|
|
80
|
+
public static getInstance(): MicSessionManager {
|
|
81
|
+
if (!MicSessionManager.instance) {
|
|
82
|
+
MicSessionManager.instance = new MicSessionManager()
|
|
83
|
+
}
|
|
84
|
+
return MicSessionManager.instance
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Test seam. Production reads the platform once at construction. */
|
|
88
|
+
public setPlatformCaps(caps: MicPlatformCaps): void {
|
|
89
|
+
this.caps = caps
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Take a microphone lease.
|
|
94
|
+
*
|
|
95
|
+
* Throws [MIC_SOURCE_CONFLICT] when a different source is already live: only
|
|
96
|
+
* the glasses can be pinned, so silently mixing sources would leave one
|
|
97
|
+
* consumer reading a microphone it did not ask for. That is the failure ACS
|
|
98
|
+
* already guards against frame by frame.
|
|
99
|
+
*/
|
|
100
|
+
public acquire(options: MicSessionOptions): MicSession {
|
|
101
|
+
const conflicting = [...this.sessions.values()].find((s) => s.source !== options.source)
|
|
102
|
+
if (conflicting) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
`${MIC_SOURCE_CONFLICT}: ${options.owner} asked for "${options.source}" while ` +
|
|
105
|
+
`${conflicting.owner} holds "${conflicting.source}"`,
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const id = this.nextId++
|
|
110
|
+
this.sessions.set(id, {id, owner: options.owner, source: options.source, useCase: options.useCase})
|
|
111
|
+
console.log(`${LOG_TAG}: acquire #${id} ${options.owner} ${options.useCase}/${options.source}`)
|
|
112
|
+
this.recompute()
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
id,
|
|
116
|
+
owner: options.owner,
|
|
117
|
+
source: options.source,
|
|
118
|
+
useCase: options.useCase,
|
|
119
|
+
release: () => this.releaseId(id),
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Drop every lease an owner holds. The backstop for unregister / disconnect. */
|
|
124
|
+
public releaseOwner(owner: string): boolean {
|
|
125
|
+
let removed = false
|
|
126
|
+
for (const [id, session] of this.sessions) {
|
|
127
|
+
if (session.owner !== owner) continue
|
|
128
|
+
this.sessions.delete(id)
|
|
129
|
+
removed = true
|
|
130
|
+
}
|
|
131
|
+
if (removed) {
|
|
132
|
+
console.log(`${LOG_TAG}: released all sessions for ${owner}`)
|
|
133
|
+
this.recompute()
|
|
134
|
+
}
|
|
135
|
+
return removed
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Whether an owner holds a glasses lease.
|
|
140
|
+
*
|
|
141
|
+
* Ownership only. Whether that lease has any hardware effect is
|
|
142
|
+
* [resolveMicPolicy]'s business, so on a platform without a glasses PCM
|
|
143
|
+
* uplink this still answers true while nothing is claimed.
|
|
144
|
+
*/
|
|
145
|
+
public hasGlassesSession(owner: string): boolean {
|
|
146
|
+
for (const session of this.sessions.values()) {
|
|
147
|
+
if (session.owner === owner && session.source === "glasses") return true
|
|
148
|
+
}
|
|
149
|
+
return false
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
public releaseAll(): void {
|
|
153
|
+
if (this.sessions.size === 0) {
|
|
154
|
+
this.stopSweep("released_all")
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
this.sessions.clear()
|
|
158
|
+
console.log(`${LOG_TAG}: released all sessions`)
|
|
159
|
+
this.recompute()
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
public cleanup(): void {
|
|
163
|
+
this.stopSweep("cleanup")
|
|
164
|
+
this.sessions.clear()
|
|
165
|
+
this.pinned = false
|
|
166
|
+
this.sweepFinished = false
|
|
167
|
+
MicSessionManager.instance = null
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Tests disable the auto-walk so acquire still means "policy 14". */
|
|
171
|
+
public setCallGainSweepEnabled(enabled: boolean): void {
|
|
172
|
+
this.sweepEnabled = enabled
|
|
173
|
+
if (!enabled) this.stopSweep("disabled")
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Start or restart the 15→14→15→13 comparison on a live voice_call session.
|
|
178
|
+
* Super Mode uses this so a call already in progress can be measured.
|
|
179
|
+
*/
|
|
180
|
+
public startCallGainSweep(): boolean {
|
|
181
|
+
if (!this.hasVoiceCallGlasses()) {
|
|
182
|
+
console.warn(`${LOG_TAG}: CALL_GAIN_SWEEP ignored — no live voice_call glasses session`)
|
|
183
|
+
return false
|
|
184
|
+
}
|
|
185
|
+
if (micStateCoordinator.hasConfiguredMicTuning()) {
|
|
186
|
+
console.warn(
|
|
187
|
+
`${LOG_TAG}: CALL_GAIN_SWEEP Super Mode mic-tuning is set; reset it or the glasses will ignore the sweep`,
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
this.sweepFinished = false
|
|
191
|
+
return getCallGainSweep().restart(
|
|
192
|
+
(gain) => micStateCoordinator.setSessionMicTuning({gain}),
|
|
193
|
+
() => {
|
|
194
|
+
this.sweepFinished = true
|
|
195
|
+
this.recompute()
|
|
196
|
+
},
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private releaseId(id: number): void {
|
|
201
|
+
if (!this.sessions.delete(id)) return
|
|
202
|
+
console.log(`${LOG_TAG}: release #${id}`)
|
|
203
|
+
this.recompute()
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private hasVoiceCallGlasses(): boolean {
|
|
207
|
+
if (!this.caps.glassesPcmUplink) return false
|
|
208
|
+
for (const session of this.sessions.values()) {
|
|
209
|
+
if (session.useCase === "voice_call" && session.source === "glasses") return true
|
|
210
|
+
}
|
|
211
|
+
return false
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
private stopSweep(reason: string): void {
|
|
215
|
+
if (getCallGainSweep().isActive()) getCallGainSweep().stop(reason)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private recompute(): void {
|
|
219
|
+
const policy = resolveMicPolicy([...this.sessions.values()], this.caps)
|
|
220
|
+
const voiceCall = this.hasVoiceCallGlasses()
|
|
221
|
+
if (!voiceCall) {
|
|
222
|
+
this.stopSweep("session_ended")
|
|
223
|
+
this.sweepFinished = false
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Pin before claiming PCM, so the first frame the claim produces is
|
|
227
|
+
// already from the right microphone and the phone mic is never opened.
|
|
228
|
+
if (policy.pinGlasses !== this.pinned) {
|
|
229
|
+
this.pinned = policy.pinGlasses
|
|
230
|
+
void Promise.resolve(BluetoothSdk.setMicSourcePin?.(policy.pinGlasses ? "glasses" : null)).catch(
|
|
231
|
+
(error) => {
|
|
232
|
+
console.warn(`${LOG_TAG}: setMicSourcePin failed`, error)
|
|
233
|
+
},
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
micStateCoordinator.setSessionRequirement(policy.rawPcm)
|
|
238
|
+
// Barrier is the only echo control left once the PCM claim turns hardware VAD off, so it
|
|
239
|
+
// rides with the claim rather than with the tuning a sweep may be rewriting.
|
|
240
|
+
micStateCoordinator.setSessionLoudnessGate(policy.loudnessGate)
|
|
241
|
+
|
|
242
|
+
if (getCallGainSweep().isActive()) {
|
|
243
|
+
const gain = getCallGainSweep().currentGain()
|
|
244
|
+
micStateCoordinator.setSessionMicTuning(gain == null ? policy.micTuning : {gain})
|
|
245
|
+
return
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (this.sweepEnabled && voiceCall && !this.sweepFinished) {
|
|
249
|
+
this.startCallGainSweep()
|
|
250
|
+
return
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
micStateCoordinator.setSessionMicTuning(policy.micTuning)
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const micSessionManager = MicSessionManager.getInstance()
|
|
258
|
+
export default micSessionManager
|
|
@@ -6,14 +6,29 @@
|
|
|
6
6
|
* Local miniapps subscribe to audio_chunk / transcription streams.
|
|
7
7
|
* This coordinator pushes the aggregate local requirement set to BluetoothSdk
|
|
8
8
|
* so the mic runs whenever at least one local consumer needs it.
|
|
9
|
+
*
|
|
10
|
+
* It also merges what the live microphone sessions require (MicSessionManager,
|
|
11
|
+
* via `setSessionRequirement` / `setSessionMicTuning`) with the OS preferences
|
|
12
|
+
* the settings store derives. Applications go through MicSessionManager; this
|
|
13
|
+
* class is an engine implementation detail.
|
|
9
14
|
*/
|
|
10
15
|
|
|
11
16
|
import BluetoothSdk from "@mentra/bluetooth-sdk/internal"
|
|
12
17
|
|
|
13
18
|
import {createDebouncedPatchFlusher} from "../utils/debouncedPatch"
|
|
19
|
+
import type {MicTuningProfile} from "./micPolicy"
|
|
14
20
|
|
|
15
21
|
const LOG_TAG = "MIC_COORDINATOR"
|
|
16
22
|
|
|
23
|
+
/** Field-wise compare, so a profile that moves a threshold without the gain still lands. */
|
|
24
|
+
function sameProfile(a: MicTuningProfile | null, b: MicTuningProfile | null): boolean {
|
|
25
|
+
if (a === b) return true
|
|
26
|
+
if (!a || !b) return false
|
|
27
|
+
const keys = new Set([...Object.keys(a), ...Object.keys(b)]) as Set<keyof MicTuningProfile>
|
|
28
|
+
for (const key of keys) if (a[key] !== b[key]) return false
|
|
29
|
+
return true
|
|
30
|
+
}
|
|
31
|
+
|
|
17
32
|
type MicGate = "vad" | "loudnessGate"
|
|
18
33
|
|
|
19
34
|
interface GateOverride {
|
|
@@ -24,6 +39,7 @@ interface GateOverride {
|
|
|
24
39
|
interface ConfiguredMicGates {
|
|
25
40
|
vadEnabled?: boolean | null
|
|
26
41
|
loudnessGateEnabled?: boolean | null
|
|
42
|
+
micTuning?: Record<string, number> | null
|
|
27
43
|
}
|
|
28
44
|
|
|
29
45
|
/** Mic-requirement flips are debounced (300ms) and merged into one BLE write
|
|
@@ -34,6 +50,10 @@ const flushMicRequirementsPatch = createDebouncedPatchFlusher<Record<string, unk
|
|
|
34
50
|
// gate override during the debounce window, and a captured stale value
|
|
35
51
|
// must not win after that lifecycle transition.
|
|
36
52
|
const runtimePatch = micStateCoordinator.applyEffectiveGatePolicy(patch)
|
|
53
|
+
// The merged patch, not the intent that produced it. Everything above this line is a
|
|
54
|
+
// preference; this is the only record of what the glasses were actually told, and the three
|
|
55
|
+
// keys a call depends on (VAD off, Barrier on, the tuning) are only decided here at flush.
|
|
56
|
+
console.log(`${LOG_TAG}: write`, runtimePatch)
|
|
37
57
|
void Promise.resolve(BluetoothSdk.updateBluetoothSettings(runtimePatch)).catch((err) => {
|
|
38
58
|
console.error(`${LOG_TAG}: failed to apply mic requirements:`, err)
|
|
39
59
|
})
|
|
@@ -49,15 +69,32 @@ class MicStateCoordinator {
|
|
|
49
69
|
private localWantsPcm = false
|
|
50
70
|
private localWantsLc3 = false
|
|
51
71
|
/**
|
|
52
|
-
* A live
|
|
72
|
+
* A live microphone session held through MicSessionManager — a voice call today.
|
|
53
73
|
*
|
|
54
74
|
* Tracked separately from the miniapp requirement because the two have independent lifetimes:
|
|
55
75
|
* the call miniapp does not subscribe to `audio_chunk`, and a captions miniapp that stops mid-call
|
|
56
76
|
* must not take the call's microphone with it.
|
|
57
77
|
*/
|
|
58
|
-
private
|
|
78
|
+
private sessionWantsPcm = false
|
|
59
79
|
private configuredVad: boolean | undefined
|
|
60
80
|
private configuredLoudnessGate: boolean | undefined
|
|
81
|
+
/** `mic_tuning` as the settings store last derived it: `super_mode ? desired : {}`. */
|
|
82
|
+
private configuredMicTuning: Record<string, number> = {}
|
|
83
|
+
/** Tuning required by the live sessions, resolved by micPolicy. */
|
|
84
|
+
private sessionMicTuning: MicTuningProfile | null = null
|
|
85
|
+
/**
|
|
86
|
+
* Latched once a session profile has actually been written.
|
|
87
|
+
*
|
|
88
|
+
* Before that, `mic_tuning` stays out of every patch, so a device that never
|
|
89
|
+
* runs a profile does not carry the key on unrelated mic writes. After it,
|
|
90
|
+
* the OS value keeps being restated, which is what stops a reconnect after a
|
|
91
|
+
* call from resurrecting the profile.
|
|
92
|
+
*/
|
|
93
|
+
private sessionMicTuningWritten = false
|
|
94
|
+
/** Barrier required by the live sessions, or null when they have no opinion. */
|
|
95
|
+
private sessionLoudnessGate: boolean | null = null
|
|
96
|
+
/** Latched like the tuning, so the OS value is restated once a session has moved it. */
|
|
97
|
+
private sessionLoudnessGateWritten = false
|
|
61
98
|
private readonly miniappVadOverrides = new Map<string, GateOverride>()
|
|
62
99
|
private readonly miniappLoudnessGateOverrides = new Map<string, GateOverride>()
|
|
63
100
|
private overrideSequence = 0
|
|
@@ -84,26 +121,104 @@ class MicStateCoordinator {
|
|
|
84
121
|
}
|
|
85
122
|
|
|
86
123
|
/**
|
|
87
|
-
* Claim or release raw PCM on behalf of
|
|
124
|
+
* Claim or release raw PCM on behalf of the live microphone sessions.
|
|
88
125
|
*
|
|
89
|
-
*
|
|
126
|
+
* MicSessionManager only. Applications acquire a session; they do not reach past it to here.
|
|
90
127
|
* Releasing is a claim release, not a mic shutdown: if a captions miniapp still wants PCM the
|
|
91
128
|
* microphone stays on, which is the whole reason this is a separate flag rather than a setter on
|
|
92
129
|
* the local requirement.
|
|
93
130
|
*/
|
|
94
|
-
public
|
|
95
|
-
if (this.
|
|
96
|
-
this.
|
|
97
|
-
console.log(`${LOG_TAG}:
|
|
131
|
+
public setSessionRequirement(pcm: boolean): void {
|
|
132
|
+
if (this.sessionWantsPcm === pcm) return
|
|
133
|
+
this.sessionWantsPcm = pcm
|
|
134
|
+
console.log(`${LOG_TAG}: session requirement updated — pcm=${pcm}`)
|
|
135
|
+
this.applyUnion()
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Apply or drop the tuning the live sessions require.
|
|
140
|
+
*
|
|
141
|
+
* MicSessionManager only. Rides `applyUnion` so the profile and the PCM claim land in one
|
|
142
|
+
* debounced write, resolved at flush time: a session released inside the debounce window wins
|
|
143
|
+
* over the value that was queued.
|
|
144
|
+
*/
|
|
145
|
+
public setSessionMicTuning(profile: MicTuningProfile | null): void {
|
|
146
|
+
if (sameProfile(profile, this.sessionMicTuning)) return
|
|
147
|
+
this.sessionMicTuning = profile
|
|
148
|
+
if (profile) this.sessionMicTuningWritten = true
|
|
149
|
+
// Nothing was ever written, so there is nothing to restore.
|
|
150
|
+
else if (!this.sessionMicTuningWritten) return
|
|
151
|
+
console.log(`${LOG_TAG}: session mic tuning ${profile ? JSON.stringify(profile) : "cleared"}`)
|
|
152
|
+
this.applyUnion()
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Last session profile queued, or null when the OS value is in force. */
|
|
156
|
+
public getSessionMicTuning(): MicTuningProfile | null {
|
|
157
|
+
return this.sessionMicTuning
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Last session Barrier queued, or null when the OS value is in force. For call logging. */
|
|
161
|
+
public getSessionLoudnessGate(): boolean | null {
|
|
162
|
+
return this.sessionLoudnessGate
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Run or stop the center-mic loudness gate on behalf of the live sessions.
|
|
167
|
+
*
|
|
168
|
+
* MicSessionManager only, and the counterpart to the PCM claim rather than a second opinion on
|
|
169
|
+
* it: raw PCM turns hardware VAD off, which leaves Barrier as the only thing standing between
|
|
170
|
+
* the far end and its own echo. Unlike VAD this never stops the stream, so the worst a wrong
|
|
171
|
+
* threshold costs is a zeroed frame instead of a dropped word.
|
|
172
|
+
*/
|
|
173
|
+
public setSessionLoudnessGate(enabled: boolean | null): void {
|
|
174
|
+
if (this.sessionLoudnessGate === enabled) return
|
|
175
|
+
this.sessionLoudnessGate = enabled
|
|
176
|
+
if (enabled !== null) this.sessionLoudnessGateWritten = true
|
|
177
|
+
else if (!this.sessionLoudnessGateWritten) return
|
|
178
|
+
console.log(`${LOG_TAG}: session loudness gate ${enabled === null ? "cleared" : enabled}`)
|
|
98
179
|
this.applyUnion()
|
|
99
180
|
}
|
|
100
181
|
|
|
182
|
+
/**
|
|
183
|
+
* Super Mode sliders outrank a session profile. A live override means a gain
|
|
184
|
+
* sweep would write to the coordinator and the glasses would ignore it.
|
|
185
|
+
*/
|
|
186
|
+
public hasConfiguredMicTuning(): boolean {
|
|
187
|
+
return Object.keys(this.configuredMicTuning).length > 0
|
|
188
|
+
}
|
|
189
|
+
|
|
101
190
|
/**
|
|
102
191
|
* Whether anything on this device needs a continuous raw-PCM timeline. Also the condition that
|
|
103
192
|
* forces hardware VAD off: a gate that drops silence turns a call into clipped half-words.
|
|
104
193
|
*/
|
|
105
194
|
private get wantsRawPcm(): boolean {
|
|
106
|
-
return this.localWantsPcm || this.
|
|
195
|
+
return this.localWantsPcm || this.sessionWantsPcm
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* The session profile, but only when it wins.
|
|
200
|
+
*
|
|
201
|
+
* A live Super Mode tuning value outranks it: that screen is how a profile's numbers get found
|
|
202
|
+
* on a real call in the first place. Emptiness is by key count — the settings store hands back a
|
|
203
|
+
* fresh `{}` every time, so reference checks would never match.
|
|
204
|
+
*/
|
|
205
|
+
private winningSessionMicTuning(): Record<string, number> | undefined {
|
|
206
|
+
if (!this.sessionMicTuning) return undefined
|
|
207
|
+
if (Object.keys(this.configuredMicTuning).length > 0) return undefined
|
|
208
|
+
return {...this.sessionMicTuning} as Record<string, number>
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* The session's Barrier, but only when it wins.
|
|
213
|
+
*
|
|
214
|
+
* Suppressed by a live Super Mode tuning value for the same reason the gain is: that screen is
|
|
215
|
+
* the manual override, and a gate running against hand-entered thresholds is the one case where
|
|
216
|
+
* the session's numbers are the wrong ones.
|
|
217
|
+
*/
|
|
218
|
+
private winningSessionLoudnessGate(): boolean | undefined {
|
|
219
|
+
if (this.sessionLoudnessGate === null) return undefined
|
|
220
|
+
if (Object.keys(this.configuredMicTuning).length > 0) return undefined
|
|
221
|
+
return this.sessionLoudnessGate
|
|
107
222
|
}
|
|
108
223
|
|
|
109
224
|
/**
|
|
@@ -168,6 +283,10 @@ class MicStateCoordinator {
|
|
|
168
283
|
: undefined,
|
|
169
284
|
loudnessGateEnabled:
|
|
170
285
|
typeof settings.loudness_gate_enabled === "boolean" ? settings.loudness_gate_enabled : undefined,
|
|
286
|
+
micTuning:
|
|
287
|
+
settings.mic_tuning && typeof settings.mic_tuning === "object"
|
|
288
|
+
? (settings.mic_tuning as Record<string, number>)
|
|
289
|
+
: undefined,
|
|
171
290
|
})
|
|
172
291
|
|
|
173
292
|
return this.applyActiveRuntimeOverrides(settings)
|
|
@@ -195,10 +314,19 @@ class MicStateCoordinator {
|
|
|
195
314
|
} else if (vadOverride) {
|
|
196
315
|
runtimeSettings.voice_activity_detection_enabled = vadOverride.enabled
|
|
197
316
|
}
|
|
317
|
+
const sessionGate = this.winningSessionLoudnessGate()
|
|
198
318
|
if (loudnessOverride) {
|
|
199
319
|
runtimeSettings.loudness_gate_enabled = loudnessOverride.enabled
|
|
320
|
+
} else if (sessionGate !== undefined) {
|
|
321
|
+
runtimeSettings.loudness_gate_enabled = sessionGate
|
|
200
322
|
}
|
|
201
323
|
|
|
324
|
+
// BES forgets mic_tuning on disconnect, so the on-connect replay is what
|
|
325
|
+
// puts a live session's profile back.
|
|
326
|
+
const sessionTuning = this.winningSessionMicTuning()
|
|
327
|
+
if (sessionTuning) runtimeSettings.mic_tuning = sessionTuning
|
|
328
|
+
else if (this.sessionMicTuningWritten) runtimeSettings.mic_tuning = this.configuredMicTuning
|
|
329
|
+
|
|
202
330
|
return runtimeSettings
|
|
203
331
|
}
|
|
204
332
|
|
|
@@ -235,6 +363,9 @@ class MicStateCoordinator {
|
|
|
235
363
|
if (configured.loudnessGateEnabled !== undefined) {
|
|
236
364
|
this.configuredLoudnessGate = configured.loudnessGateEnabled ?? undefined
|
|
237
365
|
}
|
|
366
|
+
if (configured.micTuning !== undefined) {
|
|
367
|
+
this.configuredMicTuning = configured.micTuning ?? {}
|
|
368
|
+
}
|
|
238
369
|
}
|
|
239
370
|
|
|
240
371
|
private overridesFor(gate: MicGate): Map<string, GateOverride> {
|
|
@@ -260,11 +391,21 @@ class MicStateCoordinator {
|
|
|
260
391
|
else if (vadOverride) patch.voice_activity_detection_enabled = vadOverride.enabled
|
|
261
392
|
else if (this.configuredVad !== undefined) patch.voice_activity_detection_enabled = this.configuredVad
|
|
262
393
|
|
|
394
|
+
const sessionGate = this.winningSessionLoudnessGate()
|
|
263
395
|
if (loudnessOverride) patch.loudness_gate_enabled = loudnessOverride.enabled
|
|
396
|
+
else if (sessionGate !== undefined) patch.loudness_gate_enabled = sessionGate
|
|
264
397
|
else if (this.configuredLoudnessGate !== undefined) {
|
|
265
398
|
patch.loudness_gate_enabled = this.configuredLoudnessGate
|
|
399
|
+
} else if (this.sessionLoudnessGateWritten) {
|
|
400
|
+
// A session moved it and the device carries no preference, so restate the product default
|
|
401
|
+
// rather than leaving the call's gate running after it ended.
|
|
402
|
+
patch.loudness_gate_enabled = false
|
|
266
403
|
}
|
|
267
404
|
|
|
405
|
+
const sessionTuning = this.winningSessionMicTuning()
|
|
406
|
+
if (sessionTuning) patch.mic_tuning = sessionTuning
|
|
407
|
+
else if (this.sessionMicTuningWritten) patch.mic_tuning = this.configuredMicTuning
|
|
408
|
+
|
|
268
409
|
return patch
|
|
269
410
|
}
|
|
270
411
|
|
|
@@ -274,7 +415,9 @@ class MicStateCoordinator {
|
|
|
274
415
|
public reset(): void {
|
|
275
416
|
this.localWantsPcm = false
|
|
276
417
|
this.localWantsLc3 = false
|
|
277
|
-
this.
|
|
418
|
+
this.sessionWantsPcm = false
|
|
419
|
+
this.sessionMicTuning = null
|
|
420
|
+
this.sessionLoudnessGate = null
|
|
278
421
|
this.applyUnion()
|
|
279
422
|
}
|
|
280
423
|
|