@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
|
@@ -6,10 +6,27 @@
|
|
|
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
|
import BluetoothSdk from "@mentra/bluetooth-sdk/internal";
|
|
11
16
|
import { createDebouncedPatchFlusher } from "../utils/debouncedPatch";
|
|
12
17
|
const LOG_TAG = "MIC_COORDINATOR";
|
|
18
|
+
/** Field-wise compare, so a profile that moves a threshold without the gain still lands. */
|
|
19
|
+
function sameProfile(a, b) {
|
|
20
|
+
if (a === b)
|
|
21
|
+
return true;
|
|
22
|
+
if (!a || !b)
|
|
23
|
+
return false;
|
|
24
|
+
const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
25
|
+
for (const key of keys)
|
|
26
|
+
if (a[key] !== b[key])
|
|
27
|
+
return false;
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
13
30
|
/** Mic-requirement flips are debounced (300ms) and merged into one BLE write
|
|
14
31
|
* (wire v2 keeps BLE JSON small and infrequent). */
|
|
15
32
|
const flushMicRequirementsPatch = createDebouncedPatchFlusher((patch) => {
|
|
@@ -18,6 +35,10 @@ const flushMicRequirementsPatch = createDebouncedPatchFlusher((patch) => {
|
|
|
18
35
|
// gate override during the debounce window, and a captured stale value
|
|
19
36
|
// must not win after that lifecycle transition.
|
|
20
37
|
const runtimePatch = micStateCoordinator.applyEffectiveGatePolicy(patch);
|
|
38
|
+
// The merged patch, not the intent that produced it. Everything above this line is a
|
|
39
|
+
// preference; this is the only record of what the glasses were actually told, and the three
|
|
40
|
+
// keys a call depends on (VAD off, Barrier on, the tuning) are only decided here at flush.
|
|
41
|
+
console.log(`${LOG_TAG}: write`, runtimePatch);
|
|
21
42
|
void Promise.resolve(BluetoothSdk.updateBluetoothSettings(runtimePatch)).catch((err) => {
|
|
22
43
|
console.error(`${LOG_TAG}: failed to apply mic requirements:`, err);
|
|
23
44
|
});
|
|
@@ -32,15 +53,32 @@ class MicStateCoordinator {
|
|
|
32
53
|
localWantsPcm = false;
|
|
33
54
|
localWantsLc3 = false;
|
|
34
55
|
/**
|
|
35
|
-
* A live
|
|
56
|
+
* A live microphone session held through MicSessionManager — a voice call today.
|
|
36
57
|
*
|
|
37
58
|
* Tracked separately from the miniapp requirement because the two have independent lifetimes:
|
|
38
59
|
* the call miniapp does not subscribe to `audio_chunk`, and a captions miniapp that stops mid-call
|
|
39
60
|
* must not take the call's microphone with it.
|
|
40
61
|
*/
|
|
41
|
-
|
|
62
|
+
sessionWantsPcm = false;
|
|
42
63
|
configuredVad;
|
|
43
64
|
configuredLoudnessGate;
|
|
65
|
+
/** `mic_tuning` as the settings store last derived it: `super_mode ? desired : {}`. */
|
|
66
|
+
configuredMicTuning = {};
|
|
67
|
+
/** Tuning required by the live sessions, resolved by micPolicy. */
|
|
68
|
+
sessionMicTuning = null;
|
|
69
|
+
/**
|
|
70
|
+
* Latched once a session profile has actually been written.
|
|
71
|
+
*
|
|
72
|
+
* Before that, `mic_tuning` stays out of every patch, so a device that never
|
|
73
|
+
* runs a profile does not carry the key on unrelated mic writes. After it,
|
|
74
|
+
* the OS value keeps being restated, which is what stops a reconnect after a
|
|
75
|
+
* call from resurrecting the profile.
|
|
76
|
+
*/
|
|
77
|
+
sessionMicTuningWritten = false;
|
|
78
|
+
/** Barrier required by the live sessions, or null when they have no opinion. */
|
|
79
|
+
sessionLoudnessGate = null;
|
|
80
|
+
/** Latched like the tuning, so the OS value is restated once a session has moved it. */
|
|
81
|
+
sessionLoudnessGateWritten = false;
|
|
44
82
|
miniappVadOverrides = new Map();
|
|
45
83
|
miniappLoudnessGateOverrides = new Map();
|
|
46
84
|
overrideSequence = 0;
|
|
@@ -63,26 +101,107 @@ class MicStateCoordinator {
|
|
|
63
101
|
this.applyUnion();
|
|
64
102
|
}
|
|
65
103
|
/**
|
|
66
|
-
* Claim or release raw PCM on behalf of
|
|
104
|
+
* Claim or release raw PCM on behalf of the live microphone sessions.
|
|
67
105
|
*
|
|
68
|
-
*
|
|
106
|
+
* MicSessionManager only. Applications acquire a session; they do not reach past it to here.
|
|
69
107
|
* Releasing is a claim release, not a mic shutdown: if a captions miniapp still wants PCM the
|
|
70
108
|
* microphone stays on, which is the whole reason this is a separate flag rather than a setter on
|
|
71
109
|
* the local requirement.
|
|
72
110
|
*/
|
|
73
|
-
|
|
74
|
-
if (this.
|
|
111
|
+
setSessionRequirement(pcm) {
|
|
112
|
+
if (this.sessionWantsPcm === pcm)
|
|
113
|
+
return;
|
|
114
|
+
this.sessionWantsPcm = pcm;
|
|
115
|
+
console.log(`${LOG_TAG}: session requirement updated — pcm=${pcm}`);
|
|
116
|
+
this.applyUnion();
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Apply or drop the tuning the live sessions require.
|
|
120
|
+
*
|
|
121
|
+
* MicSessionManager only. Rides `applyUnion` so the profile and the PCM claim land in one
|
|
122
|
+
* debounced write, resolved at flush time: a session released inside the debounce window wins
|
|
123
|
+
* over the value that was queued.
|
|
124
|
+
*/
|
|
125
|
+
setSessionMicTuning(profile) {
|
|
126
|
+
if (sameProfile(profile, this.sessionMicTuning))
|
|
127
|
+
return;
|
|
128
|
+
this.sessionMicTuning = profile;
|
|
129
|
+
if (profile)
|
|
130
|
+
this.sessionMicTuningWritten = true;
|
|
131
|
+
// Nothing was ever written, so there is nothing to restore.
|
|
132
|
+
else if (!this.sessionMicTuningWritten)
|
|
133
|
+
return;
|
|
134
|
+
console.log(`${LOG_TAG}: session mic tuning ${profile ? JSON.stringify(profile) : "cleared"}`);
|
|
135
|
+
this.applyUnion();
|
|
136
|
+
}
|
|
137
|
+
/** Last session profile queued, or null when the OS value is in force. */
|
|
138
|
+
getSessionMicTuning() {
|
|
139
|
+
return this.sessionMicTuning;
|
|
140
|
+
}
|
|
141
|
+
/** Last session Barrier queued, or null when the OS value is in force. For call logging. */
|
|
142
|
+
getSessionLoudnessGate() {
|
|
143
|
+
return this.sessionLoudnessGate;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Run or stop the center-mic loudness gate on behalf of the live sessions.
|
|
147
|
+
*
|
|
148
|
+
* MicSessionManager only, and the counterpart to the PCM claim rather than a second opinion on
|
|
149
|
+
* it: raw PCM turns hardware VAD off, which leaves Barrier as the only thing standing between
|
|
150
|
+
* the far end and its own echo. Unlike VAD this never stops the stream, so the worst a wrong
|
|
151
|
+
* threshold costs is a zeroed frame instead of a dropped word.
|
|
152
|
+
*/
|
|
153
|
+
setSessionLoudnessGate(enabled) {
|
|
154
|
+
if (this.sessionLoudnessGate === enabled)
|
|
155
|
+
return;
|
|
156
|
+
this.sessionLoudnessGate = enabled;
|
|
157
|
+
if (enabled !== null)
|
|
158
|
+
this.sessionLoudnessGateWritten = true;
|
|
159
|
+
else if (!this.sessionLoudnessGateWritten)
|
|
75
160
|
return;
|
|
76
|
-
|
|
77
|
-
console.log(`${LOG_TAG}: call requirement updated — pcm=${pcm}`);
|
|
161
|
+
console.log(`${LOG_TAG}: session loudness gate ${enabled === null ? "cleared" : enabled}`);
|
|
78
162
|
this.applyUnion();
|
|
79
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* Super Mode sliders outrank a session profile. A live override means a gain
|
|
166
|
+
* sweep would write to the coordinator and the glasses would ignore it.
|
|
167
|
+
*/
|
|
168
|
+
hasConfiguredMicTuning() {
|
|
169
|
+
return Object.keys(this.configuredMicTuning).length > 0;
|
|
170
|
+
}
|
|
80
171
|
/**
|
|
81
172
|
* Whether anything on this device needs a continuous raw-PCM timeline. Also the condition that
|
|
82
173
|
* forces hardware VAD off: a gate that drops silence turns a call into clipped half-words.
|
|
83
174
|
*/
|
|
84
175
|
get wantsRawPcm() {
|
|
85
|
-
return this.localWantsPcm || this.
|
|
176
|
+
return this.localWantsPcm || this.sessionWantsPcm;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* The session profile, but only when it wins.
|
|
180
|
+
*
|
|
181
|
+
* A live Super Mode tuning value outranks it: that screen is how a profile's numbers get found
|
|
182
|
+
* on a real call in the first place. Emptiness is by key count — the settings store hands back a
|
|
183
|
+
* fresh `{}` every time, so reference checks would never match.
|
|
184
|
+
*/
|
|
185
|
+
winningSessionMicTuning() {
|
|
186
|
+
if (!this.sessionMicTuning)
|
|
187
|
+
return undefined;
|
|
188
|
+
if (Object.keys(this.configuredMicTuning).length > 0)
|
|
189
|
+
return undefined;
|
|
190
|
+
return { ...this.sessionMicTuning };
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* The session's Barrier, but only when it wins.
|
|
194
|
+
*
|
|
195
|
+
* Suppressed by a live Super Mode tuning value for the same reason the gain is: that screen is
|
|
196
|
+
* the manual override, and a gate running against hand-entered thresholds is the one case where
|
|
197
|
+
* the session's numbers are the wrong ones.
|
|
198
|
+
*/
|
|
199
|
+
winningSessionLoudnessGate() {
|
|
200
|
+
if (this.sessionLoudnessGate === null)
|
|
201
|
+
return undefined;
|
|
202
|
+
if (Object.keys(this.configuredMicTuning).length > 0)
|
|
203
|
+
return undefined;
|
|
204
|
+
return this.sessionLoudnessGate;
|
|
86
205
|
}
|
|
87
206
|
/**
|
|
88
207
|
* Apply a miniapp-owned gate override without changing the OS preference.
|
|
@@ -139,6 +258,9 @@ class MicStateCoordinator {
|
|
|
139
258
|
? settings.voice_activity_detection_enabled
|
|
140
259
|
: undefined,
|
|
141
260
|
loudnessGateEnabled: typeof settings.loudness_gate_enabled === "boolean" ? settings.loudness_gate_enabled : undefined,
|
|
261
|
+
micTuning: settings.mic_tuning && typeof settings.mic_tuning === "object"
|
|
262
|
+
? settings.mic_tuning
|
|
263
|
+
: undefined,
|
|
142
264
|
});
|
|
143
265
|
return this.applyActiveRuntimeOverrides(settings);
|
|
144
266
|
}
|
|
@@ -163,9 +285,20 @@ class MicStateCoordinator {
|
|
|
163
285
|
else if (vadOverride) {
|
|
164
286
|
runtimeSettings.voice_activity_detection_enabled = vadOverride.enabled;
|
|
165
287
|
}
|
|
288
|
+
const sessionGate = this.winningSessionLoudnessGate();
|
|
166
289
|
if (loudnessOverride) {
|
|
167
290
|
runtimeSettings.loudness_gate_enabled = loudnessOverride.enabled;
|
|
168
291
|
}
|
|
292
|
+
else if (sessionGate !== undefined) {
|
|
293
|
+
runtimeSettings.loudness_gate_enabled = sessionGate;
|
|
294
|
+
}
|
|
295
|
+
// BES forgets mic_tuning on disconnect, so the on-connect replay is what
|
|
296
|
+
// puts a live session's profile back.
|
|
297
|
+
const sessionTuning = this.winningSessionMicTuning();
|
|
298
|
+
if (sessionTuning)
|
|
299
|
+
runtimeSettings.mic_tuning = sessionTuning;
|
|
300
|
+
else if (this.sessionMicTuningWritten)
|
|
301
|
+
runtimeSettings.mic_tuning = this.configuredMicTuning;
|
|
169
302
|
return runtimeSettings;
|
|
170
303
|
}
|
|
171
304
|
/**
|
|
@@ -197,6 +330,9 @@ class MicStateCoordinator {
|
|
|
197
330
|
if (configured.loudnessGateEnabled !== undefined) {
|
|
198
331
|
this.configuredLoudnessGate = configured.loudnessGateEnabled ?? undefined;
|
|
199
332
|
}
|
|
333
|
+
if (configured.micTuning !== undefined) {
|
|
334
|
+
this.configuredMicTuning = configured.micTuning ?? {};
|
|
335
|
+
}
|
|
200
336
|
}
|
|
201
337
|
overridesFor(gate) {
|
|
202
338
|
return gate === "vad" ? this.miniappVadOverrides : this.miniappLoudnessGateOverrides;
|
|
@@ -221,11 +357,24 @@ class MicStateCoordinator {
|
|
|
221
357
|
patch.voice_activity_detection_enabled = vadOverride.enabled;
|
|
222
358
|
else if (this.configuredVad !== undefined)
|
|
223
359
|
patch.voice_activity_detection_enabled = this.configuredVad;
|
|
360
|
+
const sessionGate = this.winningSessionLoudnessGate();
|
|
224
361
|
if (loudnessOverride)
|
|
225
362
|
patch.loudness_gate_enabled = loudnessOverride.enabled;
|
|
363
|
+
else if (sessionGate !== undefined)
|
|
364
|
+
patch.loudness_gate_enabled = sessionGate;
|
|
226
365
|
else if (this.configuredLoudnessGate !== undefined) {
|
|
227
366
|
patch.loudness_gate_enabled = this.configuredLoudnessGate;
|
|
228
367
|
}
|
|
368
|
+
else if (this.sessionLoudnessGateWritten) {
|
|
369
|
+
// A session moved it and the device carries no preference, so restate the product default
|
|
370
|
+
// rather than leaving the call's gate running after it ended.
|
|
371
|
+
patch.loudness_gate_enabled = false;
|
|
372
|
+
}
|
|
373
|
+
const sessionTuning = this.winningSessionMicTuning();
|
|
374
|
+
if (sessionTuning)
|
|
375
|
+
patch.mic_tuning = sessionTuning;
|
|
376
|
+
else if (this.sessionMicTuningWritten)
|
|
377
|
+
patch.mic_tuning = this.configuredMicTuning;
|
|
229
378
|
return patch;
|
|
230
379
|
}
|
|
231
380
|
/**
|
|
@@ -234,7 +383,9 @@ class MicStateCoordinator {
|
|
|
234
383
|
reset() {
|
|
235
384
|
this.localWantsPcm = false;
|
|
236
385
|
this.localWantsLc3 = false;
|
|
237
|
-
this.
|
|
386
|
+
this.sessionWantsPcm = false;
|
|
387
|
+
this.sessionMicTuning = null;
|
|
388
|
+
this.sessionLoudnessGate = null;
|
|
238
389
|
this.applyUnion();
|
|
239
390
|
}
|
|
240
391
|
cleanup() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MicStateCoordinator.js","sourceRoot":"","sources":["../../src/services/MicStateCoordinator.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * MicStateCoordinator\n *\n * Owns local-miniapp-driven microphone requirements.\n *\n * Local miniapps subscribe to audio_chunk / transcription streams.\n * This coordinator pushes the aggregate local requirement set to BluetoothSdk\n * so the mic runs whenever at least one local consumer needs it.\n */\n\nimport BluetoothSdk from \"@mentra/bluetooth-sdk/internal\"\n\nimport {createDebouncedPatchFlusher} from \"../utils/debouncedPatch\"\n\nconst LOG_TAG = \"MIC_COORDINATOR\"\n\ntype MicGate = \"vad\" | \"loudnessGate\"\n\ninterface GateOverride {\n enabled: boolean\n order: number\n}\n\ninterface ConfiguredMicGates {\n vadEnabled?: boolean | null\n loudnessGateEnabled?: boolean | null\n}\n\n/** Mic-requirement flips are debounced (300ms) and merged into one BLE write\n * (wire v2 keeps BLE JSON small and infrequent). */\nconst flushMicRequirementsPatch = createDebouncedPatchFlusher<Record<string, unknown>>((patch) => {\n try {\n // Resolve runtime overrides at flush time. A miniapp can acquire/release a\n // gate override during the debounce window, and a captured stale value\n // must not win after that lifecycle transition.\n const runtimePatch = micStateCoordinator.applyEffectiveGatePolicy(patch)\n void Promise.resolve(BluetoothSdk.updateBluetoothSettings(runtimePatch)).catch((err) => {\n console.error(`${LOG_TAG}: failed to apply mic requirements:`, err)\n })\n } catch (err) {\n console.error(`${LOG_TAG}: failed to apply mic requirements:`, err)\n }\n}, 300)\n\nclass MicStateCoordinator {\n private static instance: MicStateCoordinator | null = null\n\n // Local miniapp requirements (set when miniapps subscribe to audio streams)\n private localWantsPcm = false\n private localWantsLc3 = false\n /**\n * A live ACS call taking the wearer's voice off the glasses over BLE LC3.\n *\n * Tracked separately from the miniapp requirement because the two have independent lifetimes:\n * the call miniapp does not subscribe to `audio_chunk`, and a captions miniapp that stops mid-call\n * must not take the call's microphone with it.\n */\n private callWantsPcm = false\n private configuredVad: boolean | undefined\n private configuredLoudnessGate: boolean | undefined\n private readonly miniappVadOverrides = new Map<string, GateOverride>()\n private readonly miniappLoudnessGateOverrides = new Map<string, GateOverride>()\n private overrideSequence = 0\n\n private constructor() {}\n\n public static getInstance(): MicStateCoordinator {\n if (!MicStateCoordinator.instance) {\n MicStateCoordinator.instance = new MicStateCoordinator()\n }\n return MicStateCoordinator.instance\n }\n\n /**\n * Update local miniapp requirements. Called by LocalMiniappRuntime when\n * the aggregated set of local subscriptions changes.\n */\n public setLocalRequirements(req: {pcm: boolean; lc3: boolean} & ConfiguredMicGates): void {\n this.localWantsPcm = req.pcm\n this.localWantsLc3 = req.lc3\n this.rememberConfiguredGates(req)\n console.log(`${LOG_TAG}: local requirements updated — pcm=${req.pcm} lc3=${req.lc3}`)\n this.applyUnion()\n }\n\n /**\n * Claim or release raw PCM on behalf of an active call.\n *\n * Called by AcsMeetingService around a call whose uplink is the glasses microphone over BLE LC3.\n * Releasing is a claim release, not a mic shutdown: if a captions miniapp still wants PCM the\n * microphone stays on, which is the whole reason this is a separate flag rather than a setter on\n * the local requirement.\n */\n public setCallRequirement(pcm: boolean): void {\n if (this.callWantsPcm === pcm) return\n this.callWantsPcm = pcm\n console.log(`${LOG_TAG}: call requirement updated — pcm=${pcm}`)\n this.applyUnion()\n }\n\n /**\n * Whether anything on this device needs a continuous raw-PCM timeline. Also the condition that\n * forces hardware VAD off: a gate that drops silence turns a call into clipped half-words.\n */\n private get wantsRawPcm(): boolean {\n return this.localWantsPcm || this.callWantsPcm\n }\n\n /**\n * Apply a miniapp-owned gate override without changing the OS preference.\n * Overrides are lifecycle-scoped and last-live-owner-wins independently for\n * VAD and Barrier.\n */\n public async setMiniappGateOverride(\n packageName: string,\n gate: MicGate,\n enabled: boolean,\n configured: ConfiguredMicGates = {},\n ): Promise<void> {\n this.rememberConfiguredGates(configured)\n const overrides = this.overridesFor(gate)\n const previous = overrides.get(packageName)\n const next = {enabled, order: ++this.overrideSequence}\n overrides.set(packageName, next)\n\n try {\n await BluetoothSdk.updateBluetoothSettings(this.effectiveGatePatch())\n } catch (error) {\n // Do not roll back a newer request from the same package that landed\n // while this native write was in flight.\n if (overrides.get(packageName) === next) {\n if (previous) overrides.set(packageName, previous)\n else overrides.delete(packageName)\n }\n throw error\n }\n }\n\n /**\n * Remove every gate override owned by a miniapp. This is synchronous so\n * unregister can drop ownership before recomputing aggregate mic state.\n */\n public clearMiniappGateOverrides(packageName: string): boolean {\n const removedVad = this.miniappVadOverrides.delete(packageName)\n const removedLoudness = this.miniappLoudnessGateOverrides.delete(packageName)\n return removedVad || removedLoudness\n }\n\n /** Re-apply the current runtime policy after an owner is released. */\n public async syncEffectiveGatePolicy(configured: ConfiguredMicGates = {}): Promise<void> {\n this.rememberConfiguredGates(configured)\n const patch = this.effectiveGatePatch()\n if (Object.keys(patch).length === 0) return\n await BluetoothSdk.updateBluetoothSettings(patch)\n }\n\n /**\n * Preserve the runtime microphone contract when the persisted device\n * settings are replayed (for example after a glasses reconnect). Active\n * miniapp gate overrides replace the OS values, and raw PCM keeps VAD off\n * until the last raw-audio consumer unsubscribes.\n */\n public applyRuntimeOverrides(settings: Record<string, unknown>): Record<string, unknown> {\n this.rememberConfiguredGates({\n vadEnabled:\n typeof settings.voice_activity_detection_enabled === \"boolean\"\n ? settings.voice_activity_detection_enabled\n : undefined,\n loudnessGateEnabled:\n typeof settings.loudness_gate_enabled === \"boolean\" ? settings.loudness_gate_enabled : undefined,\n })\n\n return this.applyActiveRuntimeOverrides(settings)\n }\n\n /**\n * Apply the complete current gate policy without treating the input as an OS\n * preference update. Used by debounced mic-requirement writes, whose queued\n * gate values may be stale after an override is acquired or released.\n */\n public applyEffectiveGatePolicy(settings: Record<string, unknown>): Record<string, unknown> {\n return {\n ...settings,\n ...this.effectiveGatePatch(),\n }\n }\n\n private applyActiveRuntimeOverrides(settings: Record<string, unknown>): Record<string, unknown> {\n const runtimeSettings = {...settings}\n const vadOverride = this.latestOverride(this.miniappVadOverrides)\n const loudnessOverride = this.latestOverride(this.miniappLoudnessGateOverrides)\n\n if (this.wantsRawPcm) {\n runtimeSettings.voice_activity_detection_enabled = false\n } else if (vadOverride) {\n runtimeSettings.voice_activity_detection_enabled = vadOverride.enabled\n }\n if (loudnessOverride) {\n runtimeSettings.loudness_gate_enabled = loudnessOverride.enabled\n }\n\n return runtimeSettings\n }\n\n /**\n * Push local requirements to BluetoothSdk. `should_send_pcm` is strictly for\n * on-device PCM consumers; cloud audio uses LC3 through AudioCloudUplink.\n */\n private applyUnion(): void {\n const shouldSendPcm = this.wantsRawPcm\n const shouldSendLc3 = this.localWantsLc3\n\n // console.log(\n // `${LOG_TAG}: applying requirements — pcm=${shouldSendPcm} lc3=${shouldSendLc3}`,\n // )\n\n // The mic control plane is a direct btsdk call now (was a host setMicRequirements\n // hook) so a bare OEM streams audio without wiring it.\n const patch: Record<string, unknown> = {\n should_send_pcm: shouldSendPcm,\n should_send_lc3: shouldSendLc3,\n should_send_transcript: false,\n ...this.effectiveGatePatch(),\n }\n\n flushMicRequirementsPatch(patch)\n }\n\n private rememberConfiguredGates(configured: ConfiguredMicGates): void {\n // null means the connected device intentionally omits that setting.\n // undefined means the caller is not updating the remembered preference.\n if (configured.vadEnabled !== undefined) {\n this.configuredVad = configured.vadEnabled ?? undefined\n }\n if (configured.loudnessGateEnabled !== undefined) {\n this.configuredLoudnessGate = configured.loudnessGateEnabled ?? undefined\n }\n }\n\n private overridesFor(gate: MicGate): Map<string, GateOverride> {\n return gate === \"vad\" ? this.miniappVadOverrides : this.miniappLoudnessGateOverrides\n }\n\n private latestOverride(overrides: Map<string, GateOverride>): GateOverride | undefined {\n let latest: GateOverride | undefined\n for (const entry of overrides.values()) {\n if (!latest || entry.order > latest.order) latest = entry\n }\n return latest\n }\n\n private effectiveGatePatch(): Record<string, unknown> {\n const patch: Record<string, unknown> = {}\n const vadOverride = this.latestOverride(this.miniappVadOverrides)\n const loudnessOverride = this.latestOverride(this.miniappLoudnessGateOverrides)\n\n // Hardware VAD suppresses silence. Raw-audio consumers need a continuous\n // timeline, so their requirement wins over both OS and miniapp VAD values.\n if (this.wantsRawPcm) patch.voice_activity_detection_enabled = false\n else if (vadOverride) patch.voice_activity_detection_enabled = vadOverride.enabled\n else if (this.configuredVad !== undefined) patch.voice_activity_detection_enabled = this.configuredVad\n\n if (loudnessOverride) patch.loudness_gate_enabled = loudnessOverride.enabled\n else if (this.configuredLoudnessGate !== undefined) {\n patch.loudness_gate_enabled = this.configuredLoudnessGate\n }\n\n return patch\n }\n\n /**\n * Reset all requirements to off. Called during cleanup.\n */\n public reset(): void {\n this.localWantsPcm = false\n this.localWantsLc3 = false\n this.callWantsPcm = false\n this.applyUnion()\n }\n\n public cleanup(): void {\n console.log(`${LOG_TAG}: cleanup()`)\n this.miniappVadOverrides.clear()\n this.miniappLoudnessGateOverrides.clear()\n this.reset()\n MicStateCoordinator.instance = null\n }\n}\n\nconst micStateCoordinator = MicStateCoordinator.getInstance()\nexport default micStateCoordinator\n"]}
|
|
1
|
+
{"version":3,"file":"MicStateCoordinator.js","sourceRoot":"","sources":["../../src/services/MicStateCoordinator.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * MicStateCoordinator\n *\n * Owns local-miniapp-driven microphone requirements.\n *\n * Local miniapps subscribe to audio_chunk / transcription streams.\n * This coordinator pushes the aggregate local requirement set to BluetoothSdk\n * so the mic runs whenever at least one local consumer needs it.\n *\n * It also merges what the live microphone sessions require (MicSessionManager,\n * via `setSessionRequirement` / `setSessionMicTuning`) with the OS preferences\n * the settings store derives. Applications go through MicSessionManager; this\n * class is an engine implementation detail.\n */\n\nimport BluetoothSdk from \"@mentra/bluetooth-sdk/internal\"\n\nimport {createDebouncedPatchFlusher} from \"../utils/debouncedPatch\"\nimport type {MicTuningProfile} from \"./micPolicy\"\n\nconst LOG_TAG = \"MIC_COORDINATOR\"\n\n/** Field-wise compare, so a profile that moves a threshold without the gain still lands. */\nfunction sameProfile(a: MicTuningProfile | null, b: MicTuningProfile | null): boolean {\n if (a === b) return true\n if (!a || !b) return false\n const keys = new Set([...Object.keys(a), ...Object.keys(b)]) as Set<keyof MicTuningProfile>\n for (const key of keys) if (a[key] !== b[key]) return false\n return true\n}\n\ntype MicGate = \"vad\" | \"loudnessGate\"\n\ninterface GateOverride {\n enabled: boolean\n order: number\n}\n\ninterface ConfiguredMicGates {\n vadEnabled?: boolean | null\n loudnessGateEnabled?: boolean | null\n micTuning?: Record<string, number> | null\n}\n\n/** Mic-requirement flips are debounced (300ms) and merged into one BLE write\n * (wire v2 keeps BLE JSON small and infrequent). */\nconst flushMicRequirementsPatch = createDebouncedPatchFlusher<Record<string, unknown>>((patch) => {\n try {\n // Resolve runtime overrides at flush time. A miniapp can acquire/release a\n // gate override during the debounce window, and a captured stale value\n // must not win after that lifecycle transition.\n const runtimePatch = micStateCoordinator.applyEffectiveGatePolicy(patch)\n // The merged patch, not the intent that produced it. Everything above this line is a\n // preference; this is the only record of what the glasses were actually told, and the three\n // keys a call depends on (VAD off, Barrier on, the tuning) are only decided here at flush.\n console.log(`${LOG_TAG}: write`, runtimePatch)\n void Promise.resolve(BluetoothSdk.updateBluetoothSettings(runtimePatch)).catch((err) => {\n console.error(`${LOG_TAG}: failed to apply mic requirements:`, err)\n })\n } catch (err) {\n console.error(`${LOG_TAG}: failed to apply mic requirements:`, err)\n }\n}, 300)\n\nclass MicStateCoordinator {\n private static instance: MicStateCoordinator | null = null\n\n // Local miniapp requirements (set when miniapps subscribe to audio streams)\n private localWantsPcm = false\n private localWantsLc3 = false\n /**\n * A live microphone session held through MicSessionManager — a voice call today.\n *\n * Tracked separately from the miniapp requirement because the two have independent lifetimes:\n * the call miniapp does not subscribe to `audio_chunk`, and a captions miniapp that stops mid-call\n * must not take the call's microphone with it.\n */\n private sessionWantsPcm = false\n private configuredVad: boolean | undefined\n private configuredLoudnessGate: boolean | undefined\n /** `mic_tuning` as the settings store last derived it: `super_mode ? desired : {}`. */\n private configuredMicTuning: Record<string, number> = {}\n /** Tuning required by the live sessions, resolved by micPolicy. */\n private sessionMicTuning: MicTuningProfile | null = null\n /**\n * Latched once a session profile has actually been written.\n *\n * Before that, `mic_tuning` stays out of every patch, so a device that never\n * runs a profile does not carry the key on unrelated mic writes. After it,\n * the OS value keeps being restated, which is what stops a reconnect after a\n * call from resurrecting the profile.\n */\n private sessionMicTuningWritten = false\n /** Barrier required by the live sessions, or null when they have no opinion. */\n private sessionLoudnessGate: boolean | null = null\n /** Latched like the tuning, so the OS value is restated once a session has moved it. */\n private sessionLoudnessGateWritten = false\n private readonly miniappVadOverrides = new Map<string, GateOverride>()\n private readonly miniappLoudnessGateOverrides = new Map<string, GateOverride>()\n private overrideSequence = 0\n\n private constructor() {}\n\n public static getInstance(): MicStateCoordinator {\n if (!MicStateCoordinator.instance) {\n MicStateCoordinator.instance = new MicStateCoordinator()\n }\n return MicStateCoordinator.instance\n }\n\n /**\n * Update local miniapp requirements. Called by LocalMiniappRuntime when\n * the aggregated set of local subscriptions changes.\n */\n public setLocalRequirements(req: {pcm: boolean; lc3: boolean} & ConfiguredMicGates): void {\n this.localWantsPcm = req.pcm\n this.localWantsLc3 = req.lc3\n this.rememberConfiguredGates(req)\n console.log(`${LOG_TAG}: local requirements updated — pcm=${req.pcm} lc3=${req.lc3}`)\n this.applyUnion()\n }\n\n /**\n * Claim or release raw PCM on behalf of the live microphone sessions.\n *\n * MicSessionManager only. Applications acquire a session; they do not reach past it to here.\n * Releasing is a claim release, not a mic shutdown: if a captions miniapp still wants PCM the\n * microphone stays on, which is the whole reason this is a separate flag rather than a setter on\n * the local requirement.\n */\n public setSessionRequirement(pcm: boolean): void {\n if (this.sessionWantsPcm === pcm) return\n this.sessionWantsPcm = pcm\n console.log(`${LOG_TAG}: session requirement updated — pcm=${pcm}`)\n this.applyUnion()\n }\n\n /**\n * Apply or drop the tuning the live sessions require.\n *\n * MicSessionManager only. Rides `applyUnion` so the profile and the PCM claim land in one\n * debounced write, resolved at flush time: a session released inside the debounce window wins\n * over the value that was queued.\n */\n public setSessionMicTuning(profile: MicTuningProfile | null): void {\n if (sameProfile(profile, this.sessionMicTuning)) return\n this.sessionMicTuning = profile\n if (profile) this.sessionMicTuningWritten = true\n // Nothing was ever written, so there is nothing to restore.\n else if (!this.sessionMicTuningWritten) return\n console.log(`${LOG_TAG}: session mic tuning ${profile ? JSON.stringify(profile) : \"cleared\"}`)\n this.applyUnion()\n }\n\n /** Last session profile queued, or null when the OS value is in force. */\n public getSessionMicTuning(): MicTuningProfile | null {\n return this.sessionMicTuning\n }\n\n /** Last session Barrier queued, or null when the OS value is in force. For call logging. */\n public getSessionLoudnessGate(): boolean | null {\n return this.sessionLoudnessGate\n }\n\n /**\n * Run or stop the center-mic loudness gate on behalf of the live sessions.\n *\n * MicSessionManager only, and the counterpart to the PCM claim rather than a second opinion on\n * it: raw PCM turns hardware VAD off, which leaves Barrier as the only thing standing between\n * the far end and its own echo. Unlike VAD this never stops the stream, so the worst a wrong\n * threshold costs is a zeroed frame instead of a dropped word.\n */\n public setSessionLoudnessGate(enabled: boolean | null): void {\n if (this.sessionLoudnessGate === enabled) return\n this.sessionLoudnessGate = enabled\n if (enabled !== null) this.sessionLoudnessGateWritten = true\n else if (!this.sessionLoudnessGateWritten) return\n console.log(`${LOG_TAG}: session loudness gate ${enabled === null ? \"cleared\" : enabled}`)\n this.applyUnion()\n }\n\n /**\n * Super Mode sliders outrank a session profile. A live override means a gain\n * sweep would write to the coordinator and the glasses would ignore it.\n */\n public hasConfiguredMicTuning(): boolean {\n return Object.keys(this.configuredMicTuning).length > 0\n }\n\n /**\n * Whether anything on this device needs a continuous raw-PCM timeline. Also the condition that\n * forces hardware VAD off: a gate that drops silence turns a call into clipped half-words.\n */\n private get wantsRawPcm(): boolean {\n return this.localWantsPcm || this.sessionWantsPcm\n }\n\n /**\n * The session profile, but only when it wins.\n *\n * A live Super Mode tuning value outranks it: that screen is how a profile's numbers get found\n * on a real call in the first place. Emptiness is by key count — the settings store hands back a\n * fresh `{}` every time, so reference checks would never match.\n */\n private winningSessionMicTuning(): Record<string, number> | undefined {\n if (!this.sessionMicTuning) return undefined\n if (Object.keys(this.configuredMicTuning).length > 0) return undefined\n return {...this.sessionMicTuning} as Record<string, number>\n }\n\n /**\n * The session's Barrier, but only when it wins.\n *\n * Suppressed by a live Super Mode tuning value for the same reason the gain is: that screen is\n * the manual override, and a gate running against hand-entered thresholds is the one case where\n * the session's numbers are the wrong ones.\n */\n private winningSessionLoudnessGate(): boolean | undefined {\n if (this.sessionLoudnessGate === null) return undefined\n if (Object.keys(this.configuredMicTuning).length > 0) return undefined\n return this.sessionLoudnessGate\n }\n\n /**\n * Apply a miniapp-owned gate override without changing the OS preference.\n * Overrides are lifecycle-scoped and last-live-owner-wins independently for\n * VAD and Barrier.\n */\n public async setMiniappGateOverride(\n packageName: string,\n gate: MicGate,\n enabled: boolean,\n configured: ConfiguredMicGates = {},\n ): Promise<void> {\n this.rememberConfiguredGates(configured)\n const overrides = this.overridesFor(gate)\n const previous = overrides.get(packageName)\n const next = {enabled, order: ++this.overrideSequence}\n overrides.set(packageName, next)\n\n try {\n await BluetoothSdk.updateBluetoothSettings(this.effectiveGatePatch())\n } catch (error) {\n // Do not roll back a newer request from the same package that landed\n // while this native write was in flight.\n if (overrides.get(packageName) === next) {\n if (previous) overrides.set(packageName, previous)\n else overrides.delete(packageName)\n }\n throw error\n }\n }\n\n /**\n * Remove every gate override owned by a miniapp. This is synchronous so\n * unregister can drop ownership before recomputing aggregate mic state.\n */\n public clearMiniappGateOverrides(packageName: string): boolean {\n const removedVad = this.miniappVadOverrides.delete(packageName)\n const removedLoudness = this.miniappLoudnessGateOverrides.delete(packageName)\n return removedVad || removedLoudness\n }\n\n /** Re-apply the current runtime policy after an owner is released. */\n public async syncEffectiveGatePolicy(configured: ConfiguredMicGates = {}): Promise<void> {\n this.rememberConfiguredGates(configured)\n const patch = this.effectiveGatePatch()\n if (Object.keys(patch).length === 0) return\n await BluetoothSdk.updateBluetoothSettings(patch)\n }\n\n /**\n * Preserve the runtime microphone contract when the persisted device\n * settings are replayed (for example after a glasses reconnect). Active\n * miniapp gate overrides replace the OS values, and raw PCM keeps VAD off\n * until the last raw-audio consumer unsubscribes.\n */\n public applyRuntimeOverrides(settings: Record<string, unknown>): Record<string, unknown> {\n this.rememberConfiguredGates({\n vadEnabled:\n typeof settings.voice_activity_detection_enabled === \"boolean\"\n ? settings.voice_activity_detection_enabled\n : undefined,\n loudnessGateEnabled:\n typeof settings.loudness_gate_enabled === \"boolean\" ? settings.loudness_gate_enabled : undefined,\n micTuning:\n settings.mic_tuning && typeof settings.mic_tuning === \"object\"\n ? (settings.mic_tuning as Record<string, number>)\n : undefined,\n })\n\n return this.applyActiveRuntimeOverrides(settings)\n }\n\n /**\n * Apply the complete current gate policy without treating the input as an OS\n * preference update. Used by debounced mic-requirement writes, whose queued\n * gate values may be stale after an override is acquired or released.\n */\n public applyEffectiveGatePolicy(settings: Record<string, unknown>): Record<string, unknown> {\n return {\n ...settings,\n ...this.effectiveGatePatch(),\n }\n }\n\n private applyActiveRuntimeOverrides(settings: Record<string, unknown>): Record<string, unknown> {\n const runtimeSettings = {...settings}\n const vadOverride = this.latestOverride(this.miniappVadOverrides)\n const loudnessOverride = this.latestOverride(this.miniappLoudnessGateOverrides)\n\n if (this.wantsRawPcm) {\n runtimeSettings.voice_activity_detection_enabled = false\n } else if (vadOverride) {\n runtimeSettings.voice_activity_detection_enabled = vadOverride.enabled\n }\n const sessionGate = this.winningSessionLoudnessGate()\n if (loudnessOverride) {\n runtimeSettings.loudness_gate_enabled = loudnessOverride.enabled\n } else if (sessionGate !== undefined) {\n runtimeSettings.loudness_gate_enabled = sessionGate\n }\n\n // BES forgets mic_tuning on disconnect, so the on-connect replay is what\n // puts a live session's profile back.\n const sessionTuning = this.winningSessionMicTuning()\n if (sessionTuning) runtimeSettings.mic_tuning = sessionTuning\n else if (this.sessionMicTuningWritten) runtimeSettings.mic_tuning = this.configuredMicTuning\n\n return runtimeSettings\n }\n\n /**\n * Push local requirements to BluetoothSdk. `should_send_pcm` is strictly for\n * on-device PCM consumers; cloud audio uses LC3 through AudioCloudUplink.\n */\n private applyUnion(): void {\n const shouldSendPcm = this.wantsRawPcm\n const shouldSendLc3 = this.localWantsLc3\n\n // console.log(\n // `${LOG_TAG}: applying requirements — pcm=${shouldSendPcm} lc3=${shouldSendLc3}`,\n // )\n\n // The mic control plane is a direct btsdk call now (was a host setMicRequirements\n // hook) so a bare OEM streams audio without wiring it.\n const patch: Record<string, unknown> = {\n should_send_pcm: shouldSendPcm,\n should_send_lc3: shouldSendLc3,\n should_send_transcript: false,\n ...this.effectiveGatePatch(),\n }\n\n flushMicRequirementsPatch(patch)\n }\n\n private rememberConfiguredGates(configured: ConfiguredMicGates): void {\n // null means the connected device intentionally omits that setting.\n // undefined means the caller is not updating the remembered preference.\n if (configured.vadEnabled !== undefined) {\n this.configuredVad = configured.vadEnabled ?? undefined\n }\n if (configured.loudnessGateEnabled !== undefined) {\n this.configuredLoudnessGate = configured.loudnessGateEnabled ?? undefined\n }\n if (configured.micTuning !== undefined) {\n this.configuredMicTuning = configured.micTuning ?? {}\n }\n }\n\n private overridesFor(gate: MicGate): Map<string, GateOverride> {\n return gate === \"vad\" ? this.miniappVadOverrides : this.miniappLoudnessGateOverrides\n }\n\n private latestOverride(overrides: Map<string, GateOverride>): GateOverride | undefined {\n let latest: GateOverride | undefined\n for (const entry of overrides.values()) {\n if (!latest || entry.order > latest.order) latest = entry\n }\n return latest\n }\n\n private effectiveGatePatch(): Record<string, unknown> {\n const patch: Record<string, unknown> = {}\n const vadOverride = this.latestOverride(this.miniappVadOverrides)\n const loudnessOverride = this.latestOverride(this.miniappLoudnessGateOverrides)\n\n // Hardware VAD suppresses silence. Raw-audio consumers need a continuous\n // timeline, so their requirement wins over both OS and miniapp VAD values.\n if (this.wantsRawPcm) patch.voice_activity_detection_enabled = false\n else if (vadOverride) patch.voice_activity_detection_enabled = vadOverride.enabled\n else if (this.configuredVad !== undefined) patch.voice_activity_detection_enabled = this.configuredVad\n\n const sessionGate = this.winningSessionLoudnessGate()\n if (loudnessOverride) patch.loudness_gate_enabled = loudnessOverride.enabled\n else if (sessionGate !== undefined) patch.loudness_gate_enabled = sessionGate\n else if (this.configuredLoudnessGate !== undefined) {\n patch.loudness_gate_enabled = this.configuredLoudnessGate\n } else if (this.sessionLoudnessGateWritten) {\n // A session moved it and the device carries no preference, so restate the product default\n // rather than leaving the call's gate running after it ended.\n patch.loudness_gate_enabled = false\n }\n\n const sessionTuning = this.winningSessionMicTuning()\n if (sessionTuning) patch.mic_tuning = sessionTuning\n else if (this.sessionMicTuningWritten) patch.mic_tuning = this.configuredMicTuning\n\n return patch\n }\n\n /**\n * Reset all requirements to off. Called during cleanup.\n */\n public reset(): void {\n this.localWantsPcm = false\n this.localWantsLc3 = false\n this.sessionWantsPcm = false\n this.sessionMicTuning = null\n this.sessionLoudnessGate = null\n this.applyUnion()\n }\n\n public cleanup(): void {\n console.log(`${LOG_TAG}: cleanup()`)\n this.miniappVadOverrides.clear()\n this.miniappLoudnessGateOverrides.clear()\n this.reset()\n MicStateCoordinator.instance = null\n }\n}\n\nconst micStateCoordinator = MicStateCoordinator.getInstance()\nexport default micStateCoordinator\n"]}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Microphone use-case policy.
|
|
3
|
+
*
|
|
4
|
+
* Applications say what they are doing ("voice_call"); the engine decides what
|
|
5
|
+
* that requires of the hardware. Nothing above this file knows that a voice
|
|
6
|
+
* call means ADC index 14, and no miniapp can ask for a gain directly.
|
|
7
|
+
*
|
|
8
|
+
* Pure: every function here is a function of the live session set. The lease
|
|
9
|
+
* bookkeeping lives in MicSessionManager, the merge with OS preferences lives
|
|
10
|
+
* in MicStateCoordinator, and the wire write lives below that again.
|
|
11
|
+
*/
|
|
12
|
+
/** What an application is doing with the microphone. */
|
|
13
|
+
export type MicUseCase = "voice_call" | "transcription" | "voice_assistant" | "diagnostic";
|
|
14
|
+
/** Which microphone the audio comes from. */
|
|
15
|
+
export type MicSource = "glasses" | "phone";
|
|
16
|
+
/** Subset of the `mic_tuning` wire payload. Fields left out keep firmware defaults. */
|
|
17
|
+
export type MicTuningProfile = {
|
|
18
|
+
/** codec_adc_vol index, 0-15. */
|
|
19
|
+
gain?: number;
|
|
20
|
+
/** Center-mic RMS to open the gate. */
|
|
21
|
+
open?: number;
|
|
22
|
+
/** Center-mic RMS to close it again. */
|
|
23
|
+
close?: number;
|
|
24
|
+
/** Open threshold while the speaker is elevated. */
|
|
25
|
+
sp_open?: number;
|
|
26
|
+
/** Close threshold while the speaker is elevated. */
|
|
27
|
+
sp_close?: number;
|
|
28
|
+
};
|
|
29
|
+
/** `codec_adc_vol[]`: index to dB. Index 0 is mute and is never offered. */
|
|
30
|
+
export declare const GAIN_DB: number[];
|
|
31
|
+
/**
|
|
32
|
+
* Mirrored from `center_mic_vad_get_default_config` and `CODEC_SADC_VOL`.
|
|
33
|
+
*
|
|
34
|
+
* The four RMS numbers are measured *after* the ADC gain, so they are only
|
|
35
|
+
* meaningful next to the gain they were calibrated at, which is index 15.
|
|
36
|
+
*/
|
|
37
|
+
export declare const MIC_TUNING_FIRMWARE_DEFAULTS: {
|
|
38
|
+
readonly gain: 15;
|
|
39
|
+
readonly open: 1350;
|
|
40
|
+
readonly close: 945;
|
|
41
|
+
readonly sp_open: 2900;
|
|
42
|
+
readonly sp_close: 1600;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Re-express the firmware's thresholds at a different gain.
|
|
46
|
+
*
|
|
47
|
+
* The gate compares post-gain RMS, so moving the gain without moving the
|
|
48
|
+
* thresholds silently changes what the gate means: at -8 dB the wearer has to
|
|
49
|
+
* be 2.5x louder to clear a number that was chosen for +32 dB. Scaling both by
|
|
50
|
+
* the same factor keeps the *acoustic* trip point the profile was tuned for,
|
|
51
|
+
* for the wearer and for speaker leak alike.
|
|
52
|
+
*/
|
|
53
|
+
export declare function scaleMicTuningToGain(gain: number): Required<MicTuningProfile>;
|
|
54
|
+
/** A live microphone lease. */
|
|
55
|
+
export type MicSessionSpec = {
|
|
56
|
+
useCase: MicUseCase;
|
|
57
|
+
source: MicSource;
|
|
58
|
+
};
|
|
59
|
+
/** What the hardware should do, given every live session. */
|
|
60
|
+
export type ResolvedMicPolicy = {
|
|
61
|
+
/** Someone needs a continuous raw-PCM timeline, which also forces hardware VAD off. */
|
|
62
|
+
rawPcm: boolean;
|
|
63
|
+
/** Pin the Bluetooth SDK to the glasses microphone. Only the glasses can be pinned. */
|
|
64
|
+
pinGlasses: boolean;
|
|
65
|
+
/** Tuning override for the glasses, or null to leave the OS value in force. */
|
|
66
|
+
micTuning: MicTuningProfile | null;
|
|
67
|
+
/** Run the center-mic loudness gate, or null to leave the OS value in force. */
|
|
68
|
+
loudnessGate: boolean | null;
|
|
69
|
+
};
|
|
70
|
+
/** Platform facts the policy cannot infer from the sessions alone. */
|
|
71
|
+
export type MicPlatformCaps = {
|
|
72
|
+
/**
|
|
73
|
+
* Whether this platform can take the wearer's voice off the glasses as raw
|
|
74
|
+
* PCM over BLE LC3. False on iOS, which has no `setMicSourcePin` and never
|
|
75
|
+
* selects the `ble-lc3` uplink.
|
|
76
|
+
*/
|
|
77
|
+
glassesPcmUplink: boolean;
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* Mentra Live ships CODEC_SADC_VOL = 15, the last entry of codec_adc_vol[] and
|
|
81
|
+
* +32 dB. That table steps 2 dB at a time everywhere except the final step,
|
|
82
|
+
* which jumps 6 dB from +26, so the default sits at the ceiling one oversized
|
|
83
|
+
* step above everything else. It suits a wearer dictating to a transcription
|
|
84
|
+
* miniapp across a room; it clips a wearer talking into a Teams call.
|
|
85
|
+
*
|
|
86
|
+
* Index 14 is +26 dB: one step down, and the step that removes the table's
|
|
87
|
+
* anomalous jump. A same-voice sweep showed 15 rails hard (2.5–4.2% clip);
|
|
88
|
+
* 14/13/12 still kiss the rail on syllable tips. 13 is +24 dB, the middle
|
|
89
|
+
* of that band, while we listen for loudness vs residual clip.
|
|
90
|
+
*
|
|
91
|
+
* The thresholds ride along with the gain rather than being stated here, so
|
|
92
|
+
* changing the index above cannot leave the gate calibrated for a level the
|
|
93
|
+
* ADC no longer produces.
|
|
94
|
+
*/
|
|
95
|
+
export declare const MIC_USE_CASE_PROFILES: Record<MicUseCase, MicTuningProfile>;
|
|
96
|
+
/**
|
|
97
|
+
* Use cases that run the center-mic loudness gate ("Barrier").
|
|
98
|
+
*
|
|
99
|
+
* A voice call is the one place it earns its keep. There is no echo canceller
|
|
100
|
+
* on the LC3 uplink — `MENTRA_LC3_SPEECH_PROCESSING` is 0 and the capture path
|
|
101
|
+
* ignores the playback reference — so without a gate the far end hears itself
|
|
102
|
+
* through the wearer's speaker. Barrier is the safe half of the firmware's two
|
|
103
|
+
* gates: it zeroes a quiet frame but never stops transmitting, where VAD drops
|
|
104
|
+
* the stream outright. Paired with the speaker-elevated thresholds above, that
|
|
105
|
+
* suppresses leak while the far end is talking and costs the wearer nothing.
|
|
106
|
+
*/
|
|
107
|
+
export declare const MIC_USE_CASE_LOUDNESS_GATE: Record<MicUseCase, boolean>;
|
|
108
|
+
/**
|
|
109
|
+
* Packages allowed to hold a `voice_call` session.
|
|
110
|
+
*
|
|
111
|
+
* Which app may make a voice call is policy, so it lives beside the profiles
|
|
112
|
+
* rather than in the request handler that enforces it.
|
|
113
|
+
*/
|
|
114
|
+
export declare const VOICE_CALL_PACKAGES: readonly string[];
|
|
115
|
+
/** Every use case, for validating an inbound request. */
|
|
116
|
+
export declare const MIC_USE_CASES: readonly MicUseCase[];
|
|
117
|
+
/** Owners of engine-internal sessions. Miniapps cannot claim these use cases. */
|
|
118
|
+
export declare const ENGINE_OWNER_PREFIX = "engine:";
|
|
119
|
+
/** Use cases only engine features may acquire. */
|
|
120
|
+
export declare const ENGINE_ONLY_USE_CASES: readonly MicUseCase[];
|
|
121
|
+
/**
|
|
122
|
+
* Resolve every live session into one hardware state.
|
|
123
|
+
*
|
|
124
|
+
* On a platform without a glasses PCM uplink a glasses session is a lease and
|
|
125
|
+
* nothing more: it satisfies ownership checks so an iOS call can still be
|
|
126
|
+
* modelled the same way, but it claims no PCM, pins nothing, and applies no
|
|
127
|
+
* gain, because the wearer's voice does not reach the call through the BES
|
|
128
|
+
* BLE path there.
|
|
129
|
+
*/
|
|
130
|
+
export declare function resolveMicPolicy(sessions: readonly MicSessionSpec[], caps: MicPlatformCaps): ResolvedMicPolicy;
|
|
131
|
+
//# sourceMappingURL=micPolicy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"micPolicy.d.ts","sourceRoot":"","sources":["../../src/services/micPolicy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,wDAAwD;AACxD,MAAM,MAAM,UAAU,GAAG,YAAY,GAAG,eAAe,GAAG,iBAAiB,GAAG,YAAY,CAAA;AAE1F,6CAA6C;AAC7C,MAAM,MAAM,SAAS,GAAG,SAAS,GAAG,OAAO,CAAA;AAE3C,uFAAuF;AACvF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,iCAAiC;IACjC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,uCAAuC;IACvC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,wCAAwC;IACxC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,oDAAoD;IACpD,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB,CAAA;AAED,4EAA4E;AAC5E,eAAO,MAAM,OAAO,UAA+D,CAAA;AAEnF;;;;;GAKG;AACH,eAAO,MAAM,4BAA4B;;;;;;CAM/B,CAAA;AAEV;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,CAAC,gBAAgB,CAAC,CAU7E;AAED,+BAA+B;AAC/B,MAAM,MAAM,cAAc,GAAG;IAC3B,OAAO,EAAE,UAAU,CAAA;IACnB,MAAM,EAAE,SAAS,CAAA;CAClB,CAAA;AAED,6DAA6D;AAC7D,MAAM,MAAM,iBAAiB,GAAG;IAC9B,uFAAuF;IACvF,MAAM,EAAE,OAAO,CAAA;IACf,uFAAuF;IACvF,UAAU,EAAE,OAAO,CAAA;IACnB,+EAA+E;IAC/E,SAAS,EAAE,gBAAgB,GAAG,IAAI,CAAA;IAClC,gFAAgF;IAChF,YAAY,EAAE,OAAO,GAAG,IAAI,CAAA;CAC7B,CAAA;AAED,sEAAsE;AACtE,MAAM,MAAM,eAAe,GAAG;IAC5B;;;;OAIG;IACH,gBAAgB,EAAE,OAAO,CAAA;CAC1B,CAAA;AAED;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,qBAAqB,EAAE,MAAM,CAAC,UAAU,EAAE,gBAAgB,CAKtE,CAAA;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,0BAA0B,EAAE,MAAM,CAAC,UAAU,EAAE,OAAO,CAKlE,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,EAAE,SAAS,MAAM,EAAwB,CAAA;AAEzE,yDAAyD;AACzD,eAAO,MAAM,aAAa,EAAE,SAAS,UAAU,EAK9C,CAAA;AAED,iFAAiF;AACjF,eAAO,MAAM,mBAAmB,YAAY,CAAA;AAE5C,kDAAkD;AAClD,eAAO,MAAM,qBAAqB,EAAE,SAAS,UAAU,EAAmB,CAAA;AAE1E;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,SAAS,cAAc,EAAE,EACnC,IAAI,EAAE,eAAe,GACpB,iBAAiB,CAwBnB"}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Microphone use-case policy.
|
|
3
|
+
*
|
|
4
|
+
* Applications say what they are doing ("voice_call"); the engine decides what
|
|
5
|
+
* that requires of the hardware. Nothing above this file knows that a voice
|
|
6
|
+
* call means ADC index 14, and no miniapp can ask for a gain directly.
|
|
7
|
+
*
|
|
8
|
+
* Pure: every function here is a function of the live session set. The lease
|
|
9
|
+
* bookkeeping lives in MicSessionManager, the merge with OS preferences lives
|
|
10
|
+
* in MicStateCoordinator, and the wire write lives below that again.
|
|
11
|
+
*/
|
|
12
|
+
/** `codec_adc_vol[]`: index to dB. Index 0 is mute and is never offered. */
|
|
13
|
+
export const GAIN_DB = [-99, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 32];
|
|
14
|
+
/**
|
|
15
|
+
* Mirrored from `center_mic_vad_get_default_config` and `CODEC_SADC_VOL`.
|
|
16
|
+
*
|
|
17
|
+
* The four RMS numbers are measured *after* the ADC gain, so they are only
|
|
18
|
+
* meaningful next to the gain they were calibrated at, which is index 15.
|
|
19
|
+
*/
|
|
20
|
+
export const MIC_TUNING_FIRMWARE_DEFAULTS = {
|
|
21
|
+
gain: 15,
|
|
22
|
+
open: 1350,
|
|
23
|
+
close: 945,
|
|
24
|
+
sp_open: 2900,
|
|
25
|
+
sp_close: 1600,
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Re-express the firmware's thresholds at a different gain.
|
|
29
|
+
*
|
|
30
|
+
* The gate compares post-gain RMS, so moving the gain without moving the
|
|
31
|
+
* thresholds silently changes what the gate means: at -8 dB the wearer has to
|
|
32
|
+
* be 2.5x louder to clear a number that was chosen for +32 dB. Scaling both by
|
|
33
|
+
* the same factor keeps the *acoustic* trip point the profile was tuned for,
|
|
34
|
+
* for the wearer and for speaker leak alike.
|
|
35
|
+
*/
|
|
36
|
+
export function scaleMicTuningToGain(gain) {
|
|
37
|
+
const index = Math.min(GAIN_DB.length - 1, Math.max(1, Math.round(gain)));
|
|
38
|
+
const factor = Math.pow(10, (GAIN_DB[index] - GAIN_DB[MIC_TUNING_FIRMWARE_DEFAULTS.gain]) / 20);
|
|
39
|
+
return {
|
|
40
|
+
gain: index,
|
|
41
|
+
open: Math.round(MIC_TUNING_FIRMWARE_DEFAULTS.open * factor),
|
|
42
|
+
close: Math.round(MIC_TUNING_FIRMWARE_DEFAULTS.close * factor),
|
|
43
|
+
sp_open: Math.round(MIC_TUNING_FIRMWARE_DEFAULTS.sp_open * factor),
|
|
44
|
+
sp_close: Math.round(MIC_TUNING_FIRMWARE_DEFAULTS.sp_close * factor),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Mentra Live ships CODEC_SADC_VOL = 15, the last entry of codec_adc_vol[] and
|
|
49
|
+
* +32 dB. That table steps 2 dB at a time everywhere except the final step,
|
|
50
|
+
* which jumps 6 dB from +26, so the default sits at the ceiling one oversized
|
|
51
|
+
* step above everything else. It suits a wearer dictating to a transcription
|
|
52
|
+
* miniapp across a room; it clips a wearer talking into a Teams call.
|
|
53
|
+
*
|
|
54
|
+
* Index 14 is +26 dB: one step down, and the step that removes the table's
|
|
55
|
+
* anomalous jump. A same-voice sweep showed 15 rails hard (2.5–4.2% clip);
|
|
56
|
+
* 14/13/12 still kiss the rail on syllable tips. 13 is +24 dB, the middle
|
|
57
|
+
* of that band, while we listen for loudness vs residual clip.
|
|
58
|
+
*
|
|
59
|
+
* The thresholds ride along with the gain rather than being stated here, so
|
|
60
|
+
* changing the index above cannot leave the gate calibrated for a level the
|
|
61
|
+
* ADC no longer produces.
|
|
62
|
+
*/
|
|
63
|
+
export const MIC_USE_CASE_PROFILES = {
|
|
64
|
+
voice_call: scaleMicTuningToGain(13),
|
|
65
|
+
transcription: {},
|
|
66
|
+
voice_assistant: {},
|
|
67
|
+
diagnostic: {},
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Use cases that run the center-mic loudness gate ("Barrier").
|
|
71
|
+
*
|
|
72
|
+
* A voice call is the one place it earns its keep. There is no echo canceller
|
|
73
|
+
* on the LC3 uplink — `MENTRA_LC3_SPEECH_PROCESSING` is 0 and the capture path
|
|
74
|
+
* ignores the playback reference — so without a gate the far end hears itself
|
|
75
|
+
* through the wearer's speaker. Barrier is the safe half of the firmware's two
|
|
76
|
+
* gates: it zeroes a quiet frame but never stops transmitting, where VAD drops
|
|
77
|
+
* the stream outright. Paired with the speaker-elevated thresholds above, that
|
|
78
|
+
* suppresses leak while the far end is talking and costs the wearer nothing.
|
|
79
|
+
*/
|
|
80
|
+
export const MIC_USE_CASE_LOUDNESS_GATE = {
|
|
81
|
+
voice_call: true,
|
|
82
|
+
transcription: false,
|
|
83
|
+
voice_assistant: false,
|
|
84
|
+
diagnostic: false,
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* Packages allowed to hold a `voice_call` session.
|
|
88
|
+
*
|
|
89
|
+
* Which app may make a voice call is policy, so it lives beside the profiles
|
|
90
|
+
* rather than in the request handler that enforces it.
|
|
91
|
+
*/
|
|
92
|
+
export const VOICE_CALL_PACKAGES = ["com.mentra.call"];
|
|
93
|
+
/** Every use case, for validating an inbound request. */
|
|
94
|
+
export const MIC_USE_CASES = [
|
|
95
|
+
"voice_call",
|
|
96
|
+
"transcription",
|
|
97
|
+
"voice_assistant",
|
|
98
|
+
"diagnostic",
|
|
99
|
+
];
|
|
100
|
+
/** Owners of engine-internal sessions. Miniapps cannot claim these use cases. */
|
|
101
|
+
export const ENGINE_OWNER_PREFIX = "engine:";
|
|
102
|
+
/** Use cases only engine features may acquire. */
|
|
103
|
+
export const ENGINE_ONLY_USE_CASES = ["diagnostic"];
|
|
104
|
+
/**
|
|
105
|
+
* Resolve every live session into one hardware state.
|
|
106
|
+
*
|
|
107
|
+
* On a platform without a glasses PCM uplink a glasses session is a lease and
|
|
108
|
+
* nothing more: it satisfies ownership checks so an iOS call can still be
|
|
109
|
+
* modelled the same way, but it claims no PCM, pins nothing, and applies no
|
|
110
|
+
* gain, because the wearer's voice does not reach the call through the BES
|
|
111
|
+
* BLE path there.
|
|
112
|
+
*/
|
|
113
|
+
export function resolveMicPolicy(sessions, caps) {
|
|
114
|
+
const effective = caps.glassesPcmUplink ? sessions : sessions.filter((s) => s.source !== "glasses");
|
|
115
|
+
let pinGlasses = false;
|
|
116
|
+
let loudnessGate = false;
|
|
117
|
+
let winning = null;
|
|
118
|
+
for (const session of effective) {
|
|
119
|
+
if (session.source !== "glasses")
|
|
120
|
+
continue;
|
|
121
|
+
pinGlasses = true;
|
|
122
|
+
if (MIC_USE_CASE_LOUDNESS_GATE[session.useCase])
|
|
123
|
+
loudnessGate = true;
|
|
124
|
+
const profile = MIC_USE_CASE_PROFILES[session.useCase];
|
|
125
|
+
if (typeof profile?.gain !== "number")
|
|
126
|
+
continue;
|
|
127
|
+
// Lowest index wins: clipping is the irreversible failure, a slightly
|
|
128
|
+
// quiet assistant is not. The whole profile travels with it, because its
|
|
129
|
+
// thresholds are only meaningful at its own gain.
|
|
130
|
+
if (winning === null || profile.gain < winning.gain)
|
|
131
|
+
winning = profile;
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
rawPcm: effective.length > 0,
|
|
135
|
+
pinGlasses,
|
|
136
|
+
micTuning: winning,
|
|
137
|
+
loudnessGate: pinGlasses ? loudnessGate : null,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
//# sourceMappingURL=micPolicy.js.map
|